Messages { get; set; }
24 |
25 | [Parameter]
26 | public ChatMessage? InProgressMessage { get; set; }
27 |
28 | [Parameter]
29 | public RenderFragment? NoMessagesContent { get; set; }
30 |
31 | private bool IsEmpty => !Messages.Any(m => (m.Role == ChatRole.User || m.Role == ChatRole.Assistant) && !string.IsNullOrEmpty(m.Text));
32 |
33 | protected override async Task OnAfterRenderAsync(bool firstRender)
34 | {
35 | if (firstRender)
36 | {
37 | // Activates the auto-scrolling behavior
38 | await JS.InvokeVoidAsync("import", "./Components/Pages/Chat/ChatMessageList.razor.js");
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatMessageList.razor.css:
--------------------------------------------------------------------------------
1 | .message-list-container {
2 | margin: 2rem 1.5rem;
3 | flex-grow: 1;
4 | }
5 |
6 | .message-list {
7 | display: flex;
8 | flex-direction: column;
9 | gap: 1.25rem;
10 | }
11 |
12 | .no-messages {
13 | text-align: center;
14 | font-size: 1.25rem;
15 | color: #999;
16 | margin-top: calc(40vh - 18rem);
17 | }
18 |
19 | chat-messages > ::deep div:last-of-type {
20 | /* Adds some vertical buffer to so that suggestions don't overlap the output when they appear */
21 | margin-bottom: 2rem;
22 | }
23 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Components/Pages/Error.razor:
--------------------------------------------------------------------------------
1 | @page "/Error"
2 | @using System.Diagnostics
3 |
4 | Error
5 |
6 | Error.
7 | An error occurred while processing your request.
8 |
9 | @if (ShowRequestId)
10 | {
11 |
12 | Request ID: @RequestId
13 |
14 | }
15 |
16 | Development Mode
17 |
18 | Swapping to Development environment will display more detailed information about the error that occurred.
19 |
20 |
21 | The Development environment shouldn't be enabled for deployed applications.
22 | It can result in displaying sensitive information from exceptions to end users.
23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
24 | and restarting the app.
25 |
26 |
27 | @code{
28 | [CascadingParameter]
29 | private HttpContext? HttpContext { get; set; }
30 |
31 | private string? RequestId { get; set; }
32 | private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
33 |
34 | protected override void OnInitialized() =>
35 | RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
36 | }
37 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Components/Routes.razor:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Components/_Imports.razor:
--------------------------------------------------------------------------------
1 | @using System.Net.Http
2 | @using System.Net.Http.Json
3 | @using Microsoft.AspNetCore.Components.Forms
4 | @using Microsoft.AspNetCore.Components.Routing
5 | @using Microsoft.AspNetCore.Components.Web
6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode
7 | @using Microsoft.AspNetCore.Components.Web.Virtualization
8 | @using Microsoft.Extensions.AI
9 | @using Microsoft.JSInterop
10 | @using OpenChat.PlaygroundApp
11 | @using OpenChat.PlaygroundApp.Components
12 | @using OpenChat.PlaygroundApp.Components.Layout
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Configurations/AmazonBedrockSettings.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 |
3 | namespace OpenChat.PlaygroundApp.Configurations;
4 |
5 | ///
6 | public partial class AppSettings
7 | {
8 | ///
9 | /// Gets or sets the instance.
10 | ///
11 | public AmazonBedrockSettings? AmazonBedrock { get; set; }
12 | }
13 |
14 | ///
15 | /// This represents the app settings entity for Amazon Bedrock.
16 | ///
17 | public class AmazonBedrockSettings : LanguageModelSettings
18 | {
19 | ///
20 | /// Gets or sets the AWSCredentials Access Key ID for the Amazon Bedrock service.
21 | ///
22 | public string? AccessKeyId { get; set; }
23 |
24 | ///
25 | /// Gets or sets the AWSCredentials Secret Access Key for the Amazon Bedrock service.
26 | ///
27 | public string? SecretAccessKey { get; set; }
28 |
29 | ///
30 | /// Gets or sets the AWS region for the Amazon Bedrock service.
31 | ///
32 | public string? Region { get; set; }
33 |
34 | ///
35 | /// Gets or sets the model ID for the Amazon Bedrock service.
36 | ///
37 | public string? ModelId { get; set; }
38 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Configurations/AnthropicSettings.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 |
3 | namespace OpenChat.PlaygroundApp.Configurations;
4 |
5 | ///
6 | public partial class AppSettings
7 | {
8 | ///
9 | /// Gets or sets the instance.
10 | ///
11 | public AnthropicSettings? Anthropic { get; set; }
12 | }
13 |
14 | ///
15 | /// This represents the app settings entity for Anthropic Claude.
16 | ///
17 | public class AnthropicSettings : LanguageModelSettings
18 | {
19 | ///
20 | /// Gets or sets the API key for Anthropic Claude.
21 | ///
22 | public string? ApiKey { get; set; }
23 |
24 | ///
25 | /// Gets or sets the model name of Anthropic Claude.
26 | ///
27 | public string? Model { get; set; }
28 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Configurations/AppSettings.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Connectors;
2 |
3 | namespace OpenChat.PlaygroundApp.Configurations;
4 |
5 | ///
6 | /// This represents the app settings entity from appsettings.json.
7 | ///
8 | public partial class AppSettings
9 | {
10 | ///
11 | /// Gets or sets the connector type to use.
12 | ///
13 | public ConnectorType ConnectorType { get; set; }
14 |
15 | ///
16 | /// Gets or sets the model name to use.
17 | ///
18 | public string? Model { get; set; }
19 |
20 | ///
21 | /// Gets or sets the value indicating whether to display help information or not.
22 | ///
23 | public bool Help { get; set; }
24 | }
25 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Configurations/AzureAIFoundrySettings.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 |
3 | namespace OpenChat.PlaygroundApp.Configurations;
4 |
5 | ///
6 | public partial class AppSettings
7 | {
8 | ///
9 | /// Gets or sets the instance.
10 | ///
11 | public AzureAIFoundrySettings? AzureAIFoundry { get; set; }
12 | }
13 |
14 | ///
15 | /// This represents the app settings entity for Azure AI Foundry.
16 | ///
17 | public class AzureAIFoundrySettings : LanguageModelSettings
18 | {
19 | ///
20 | /// Gets or sets the endpoint URL of Azure AI Foundry API.
21 | ///
22 | public string? Endpoint { get; set; }
23 |
24 | ///
25 | /// Gets or sets the Azure AI Foundry API Access Token.
26 | ///
27 | public string? ApiKey { get; set; }
28 |
29 | ///
30 | /// Gets or sets the model name of Azure AI Foundry.
31 | ///
32 | public string? DeploymentName { get; set; }
33 | }
34 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Configurations/DockerModelRunnerSettings.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 |
3 | namespace OpenChat.PlaygroundApp.Configurations;
4 |
5 | ///
6 | public partial class AppSettings
7 | {
8 | ///
9 | /// Gets or sets the instance.
10 | ///
11 | public DockerModelRunnerSettings? DockerModelRunner { get; set; }
12 | }
13 |
14 | ///
15 | /// This represents the app settings entity for Docker Model Runner.
16 | ///
17 | public class DockerModelRunnerSettings : LanguageModelSettings
18 | {
19 | ///
20 | /// Gets or sets the base URL of the Docker Model Runner API.
21 | ///
22 | public string? BaseUrl { get; set; }
23 |
24 | ///
25 | /// Gets or sets the model name of Docker Model Runner.
26 | ///
27 | public string? Model { get; set; }
28 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Configurations/FoundryLocalSettings.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 |
3 | namespace OpenChat.PlaygroundApp.Configurations;
4 |
5 | ///
6 | public partial class AppSettings
7 | {
8 | ///
9 | /// Gets or sets the instance.
10 | ///
11 | public FoundryLocalSettings? FoundryLocal { get; set; }
12 | }
13 |
14 | ///
15 | /// This represents the app settings entity for FoundryLocal.
16 | ///
17 | public class FoundryLocalSettings : LanguageModelSettings
18 | {
19 | ///
20 | /// Gets or sets the alias of FoundryLocal.
21 | ///
22 | public string? Alias { get; set; }
23 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Configurations/GitHubModelsSettings.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 |
3 | namespace OpenChat.PlaygroundApp.Configurations;
4 |
5 | ///
6 | public partial class AppSettings
7 | {
8 | ///
9 | /// Gets or sets the instance.
10 | ///
11 | public GitHubModelsSettings? GitHubModels { get; set; }
12 | }
13 |
14 | ///
15 | /// This represents the app settings entity for GitHub Models.
16 | ///
17 | public class GitHubModelsSettings : LanguageModelSettings
18 | {
19 | ///
20 | /// Gets or sets the endpoint URL of GitHub Models API.
21 | ///
22 | public string? Endpoint { get; set; }
23 |
24 | ///
25 | /// Gets or sets the GitHub Personal Access Token (PAT).
26 | ///
27 | public string? Token { get; set; }
28 |
29 | ///
30 | /// Gets or sets the model name of GitHub Models.
31 | ///
32 | public string? Model { get; set; }
33 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Configurations/GoogleVertexAISettings.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 |
3 | namespace OpenChat.PlaygroundApp.Configurations;
4 |
5 | ///
6 | public partial class AppSettings
7 | {
8 | ///
9 | /// Gets or sets the instance.
10 | ///
11 | public GoogleVertexAISettings? GoogleVertexAI { get; set; }
12 | }
13 |
14 | ///
15 | /// This represents the app settings entity for Google Vertex AI.
16 | ///
17 | public class GoogleVertexAISettings : LanguageModelSettings
18 | {
19 | ///
20 | /// Gets or sets the Google Vertex AI API Key.
21 | ///
22 | public string? ApiKey { get; set; }
23 |
24 | ///
25 | /// Gets or sets the model name of Google Vertex AI.
26 | ///
27 | public string? Model { get; set; }
28 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Configurations/HuggingFaceSettings.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 |
3 | namespace OpenChat.PlaygroundApp.Configurations;
4 |
5 | ///
6 | public partial class AppSettings
7 | {
8 | ///
9 | /// Gets or sets the instance.
10 | ///
11 | public HuggingFaceSettings? HuggingFace { get; set; }
12 | }
13 |
14 | ///
15 | /// This represents the app settings entity for Hugging Face.
16 | ///
17 | public class HuggingFaceSettings : LanguageModelSettings
18 | {
19 | ///
20 | /// Gets or sets the base URL of the Hugging Face API.
21 | ///
22 | public string? BaseUrl { get; set; }
23 |
24 | ///
25 | /// Gets or sets the model name for Hugging Face.
26 | ///
27 | public string? Model { get; set; }
28 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Configurations/LGSettings.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 |
3 | namespace OpenChat.PlaygroundApp.Configurations;
4 |
5 | ///
6 | public partial class AppSettings
7 | {
8 | ///
9 | /// Gets or sets the instance.
10 | ///
11 | public LGSettings? LG { get; set; }
12 | }
13 |
14 | ///
15 | /// This represents the app settings entity for LG AI EXAONE.
16 | ///
17 | public class LGSettings : LanguageModelSettings
18 | {
19 | ///
20 | /// Gets or sets the base URL of the LG AI EXAONE API.
21 | ///
22 | public string? BaseUrl { get; set; }
23 |
24 | ///
25 | /// Gets or sets the model name for LG AI EXAONE.
26 | ///
27 | public string? Model { get; set; }
28 | }
29 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Configurations/OllamaSettings.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 |
3 | namespace OpenChat.PlaygroundApp.Configurations;
4 |
5 | ///
6 | public partial class AppSettings
7 | {
8 | ///
9 | /// Gets or sets the instance.
10 | ///
11 | public OllamaSettings? Ollama { get; set; }
12 | }
13 |
14 | ///
15 | /// This represents the app settings entity for Ollama.
16 | ///
17 | public class OllamaSettings : LanguageModelSettings
18 | {
19 | ///
20 | /// Gets or sets the base URL of the Ollama API.
21 | ///
22 | public string? BaseUrl { get; set; }
23 |
24 | ///
25 | /// Gets or sets the model name for Ollama.
26 | ///
27 | public string? Model { get; set; }
28 | }
29 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Configurations/OpenAISettings.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 |
3 | namespace OpenChat.PlaygroundApp.Configurations;
4 |
5 | ///
6 | public partial class AppSettings
7 | {
8 | ///
9 | /// Gets or sets the instance.
10 | ///
11 | public OpenAISettings? OpenAI { get; set; }
12 | }
13 |
14 | ///
15 | /// This represents the app settings entity for OpenAI.
16 | ///
17 | public class OpenAISettings : LanguageModelSettings
18 | {
19 | ///
20 | /// Gets or sets the OpenAI API key.
21 | ///
22 | public string? ApiKey { get; set; }
23 |
24 | ///
25 | /// Gets or sets the model name for OpenAI.
26 | ///
27 | public string? Model { get; set; }
28 | }
29 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Configurations/UpstageSettings.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 |
3 | namespace OpenChat.PlaygroundApp.Configurations;
4 |
5 | ///
6 | public partial class AppSettings
7 | {
8 | ///
9 | /// Gets or sets the instance.
10 | ///
11 | public UpstageSettings? Upstage { get; set; }
12 | }
13 |
14 | ///
15 | /// This represents the app settings entity for Upstage.
16 | ///
17 | public class UpstageSettings : LanguageModelSettings
18 | {
19 | ///
20 | /// Gets or sets the base URL of Upstage API.
21 | ///
22 | public string? BaseUrl { get; set; }
23 |
24 | ///
25 | /// Gets or sets the Upstage API key.
26 | ///
27 | public string? ApiKey { get; set; }
28 |
29 | ///
30 | /// Gets or sets the model name of Upstage.
31 | ///
32 | public string? Model { get; set; }
33 | }
34 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Connectors/AzureAIFoundryConnector.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.AI;
2 |
3 | using OpenChat.PlaygroundApp.Configurations;
4 | using OpenChat.PlaygroundApp.Abstractions;
5 |
6 | using Azure;
7 | using Azure.AI.OpenAI;
8 |
9 | namespace OpenChat.PlaygroundApp.Connectors;
10 |
11 | ///
12 | /// This represents the connector entity for Azure AI Foundry.
13 | ///
14 | public class AzureAIFoundryConnector(AppSettings settings) : LanguageModelConnector(settings.AzureAIFoundry)
15 | {
16 | private readonly AppSettings _appSettings = settings ?? throw new ArgumentNullException(nameof(settings));
17 |
18 | ///
19 | public override bool EnsureLanguageModelSettingsValid()
20 | {
21 | if (this.Settings is not AzureAIFoundrySettings settings)
22 | {
23 | throw new InvalidOperationException("Missing configuration: AzureAIFoundry.");
24 | }
25 |
26 | if (string.IsNullOrWhiteSpace(settings.Endpoint?.Trim()))
27 | {
28 | throw new InvalidOperationException("Missing configuration: AzureAIFoundry:Endpoint.");
29 | }
30 |
31 | if (string.IsNullOrWhiteSpace(settings.ApiKey?.Trim()))
32 | {
33 | throw new InvalidOperationException("Missing configuration: AzureAIFoundry:ApiKey.");
34 | }
35 |
36 | if (string.IsNullOrWhiteSpace(settings.DeploymentName?.Trim()))
37 | {
38 | throw new InvalidOperationException("Missing configuration: AzureAIFoundry:DeploymentName.");
39 | }
40 |
41 | return true;
42 | }
43 |
44 | ///
45 | public override async Task GetChatClientAsync()
46 | {
47 | var settings = this.Settings as AzureAIFoundrySettings;
48 |
49 | var endpoint = new Uri(settings!.Endpoint!);
50 | var deploymentName = settings.DeploymentName!;
51 | var apiKey = settings.ApiKey!;
52 |
53 | var credential = new AzureKeyCredential(apiKey);
54 | var azureClient = new AzureOpenAIClient(endpoint, credential);
55 |
56 | var chatClient = azureClient.GetChatClient(deploymentName)
57 | .AsIChatClient();
58 |
59 | Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {settings.DeploymentName}");
60 |
61 | return await Task.FromResult(chatClient).ConfigureAwait(false);
62 | }
63 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Connectors/ConnectorType.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace OpenChat.PlaygroundApp.Connectors;
4 |
5 | ///
6 | /// This specifies the type of connector to use.
7 | ///
8 | [JsonConverter(typeof(JsonStringEnumConverter))]
9 | public enum ConnectorType
10 | {
11 | ///
12 | /// Identifies the unknown connector type.
13 | ///
14 | Unknown,
15 |
16 | ///
17 | /// Identifies the Amazon Bedrock connector type.
18 | ///
19 | AmazonBedrock,
20 |
21 | ///
22 | /// Identifies the Azure AI Foundry connector type.
23 | ///
24 | AzureAIFoundry,
25 |
26 | ///
27 | /// Identifies the GitHub Models connector type.
28 | ///
29 | GitHubModels,
30 |
31 | ///
32 | /// Identifies the Google Vertex AI connector type.
33 | ///
34 | GoogleVertexAI,
35 |
36 | ///
37 | /// Identifies the Docker Model Runner connector type.
38 | ///
39 | DockerModelRunner,
40 |
41 | ///
42 | /// Identifies the Foundry Local connector type.
43 | ///
44 | FoundryLocal,
45 |
46 | ///
47 | /// Identifies the Hugging Face connector type.
48 | ///
49 | HuggingFace,
50 |
51 | ///
52 | /// Identifies the Ollama connector type.
53 | ///
54 | Ollama,
55 |
56 | ///
57 | /// Identifies the Anthropic connector type.
58 | ///
59 | Anthropic,
60 |
61 | ///
62 | /// Identifies the LG connector type.
63 | ///
64 | LG,
65 |
66 | ///
67 | /// Identifies the Naver connector type.
68 | ///
69 | Naver,
70 |
71 | ///
72 | /// Identifies the OpenAI connector type.
73 | ///
74 | OpenAI,
75 |
76 | ///
77 | /// Identifies the Upstage connector type.
78 | ///
79 | Upstage,
80 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Connectors/GitHubModelsConnector.cs:
--------------------------------------------------------------------------------
1 | using System.ClientModel;
2 |
3 | using Microsoft.Extensions.AI;
4 |
5 | using OpenAI;
6 |
7 | using OpenChat.PlaygroundApp.Abstractions;
8 | using OpenChat.PlaygroundApp.Configurations;
9 |
10 | namespace OpenChat.PlaygroundApp.Connectors;
11 |
12 | ///
13 | /// This represents the connector entity for GitHub Models.
14 | ///
15 | /// instance.
16 | public class GitHubModelsConnector(AppSettings settings) : LanguageModelConnector(settings.GitHubModels)
17 | {
18 | private readonly AppSettings _appSettings = settings ?? throw new ArgumentNullException(nameof(settings));
19 |
20 | ///
21 | public override bool EnsureLanguageModelSettingsValid()
22 | {
23 | if (this.Settings is not GitHubModelsSettings settings)
24 | {
25 | throw new InvalidOperationException("Missing configuration: GitHubModels.");
26 | }
27 |
28 | if (string.IsNullOrWhiteSpace(settings.Endpoint!.Trim()) == true)
29 | {
30 | throw new InvalidOperationException("Missing configuration: GitHubModels:Endpoint.");
31 | }
32 |
33 | if (string.IsNullOrWhiteSpace(settings.Token!.Trim()) == true)
34 | {
35 | throw new InvalidOperationException("Missing configuration: GitHubModels:Token.");
36 | }
37 |
38 | if (string.IsNullOrWhiteSpace(settings.Model!.Trim()) == true)
39 | {
40 | throw new InvalidOperationException("Missing configuration: GitHubModels:Model.");
41 | }
42 |
43 | return true;
44 | }
45 |
46 | ///
47 | public override async Task GetChatClientAsync()
48 | {
49 | var settings = this.Settings as GitHubModelsSettings;
50 |
51 | var credential = new ApiKeyCredential(settings?.Token ?? throw new InvalidOperationException("Missing configuration: GitHubModels:Token."));
52 | var options = new OpenAIClientOptions()
53 | {
54 | Endpoint = new Uri(settings.Endpoint ?? throw new InvalidOperationException("Missing configuration: GitHubModels:Endpoint."))
55 | };
56 |
57 | var client = new OpenAIClient(credential, options);
58 | var chatClient = client.GetChatClient(settings.Model)
59 | .AsIChatClient();
60 |
61 | Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {settings.Model}");
62 |
63 | return await Task.FromResult(chatClient).ConfigureAwait(false);
64 | }
65 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Connectors/HuggingFaceConnector.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.AI;
2 |
3 | using OllamaSharp;
4 |
5 | using OpenChat.PlaygroundApp.Abstractions;
6 | using OpenChat.PlaygroundApp.Configurations;
7 |
8 | namespace OpenChat.PlaygroundApp.Connectors;
9 |
10 | ///
11 | /// This represents the connector entity for Hugging Face.
12 | ///
13 | /// instance.
14 | public class HuggingFaceConnector(AppSettings settings) : LanguageModelConnector(settings.HuggingFace)
15 | {
16 | private readonly AppSettings _appSettings = settings ?? throw new ArgumentNullException(nameof(settings));
17 |
18 | private const string HuggingFaceHost = "hf.co";
19 | private const string ModelSuffix = "gguf";
20 |
21 | ///
22 | public override bool EnsureLanguageModelSettingsValid()
23 | {
24 | if (this.Settings is not HuggingFaceSettings settings)
25 | {
26 | throw new InvalidOperationException("Missing configuration: HuggingFace.");
27 | }
28 |
29 | if (string.IsNullOrWhiteSpace(settings.BaseUrl!.Trim()) == true)
30 | {
31 | throw new InvalidOperationException("Missing configuration: HuggingFace:BaseUrl.");
32 | }
33 |
34 | if (string.IsNullOrWhiteSpace(settings.Model!.Trim()) == true)
35 | {
36 | throw new InvalidOperationException("Missing configuration: HuggingFace:Model.");
37 | }
38 |
39 | // Accepts formats like:
40 | // - hf.co/{org}/{model}gguf e.g hf.co/Qwen/Qwen3-0.6B-GGUF hf.co/Qwen/Qwen3-0.6B_GGUF
41 | if (IsValidModel(settings.Model!.Trim()) == false)
42 | {
43 | throw new InvalidOperationException("Invalid configuration: HuggingFace:Model format. Expected 'hf.co/{org}/{model}gguf' format.");
44 | }
45 |
46 | return true;
47 | }
48 |
49 | ///
50 | public override async Task GetChatClientAsync()
51 | {
52 | var settings = this.Settings as HuggingFaceSettings;
53 | var baseUrl = settings!.BaseUrl!;
54 | var model = settings!.Model!;
55 |
56 | var config = new OllamaApiClient.Configuration
57 | {
58 | Uri = new Uri(baseUrl),
59 | Model = model,
60 | };
61 |
62 | var chatClient = new OllamaApiClient(config);
63 |
64 | var pulls = chatClient.PullModelAsync(model);
65 | await foreach (var pull in pulls)
66 | {
67 | Console.WriteLine($"Pull status: {pull!.Status}");
68 | }
69 |
70 | Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {settings.Model}");
71 |
72 | return await Task.FromResult(chatClient).ConfigureAwait(false);
73 | }
74 |
75 | private static bool IsValidModel(string model)
76 | {
77 | var segments = model.Split([ '/' ], StringSplitOptions.RemoveEmptyEntries);
78 |
79 | if (segments.Length != 3)
80 | {
81 | return false;
82 | }
83 |
84 | if (segments.First().Equals(HuggingFaceHost, StringComparison.InvariantCultureIgnoreCase) == false)
85 | {
86 | return false;
87 | }
88 |
89 | if (segments.Last().EndsWith(ModelSuffix, StringComparison.InvariantCultureIgnoreCase) == false)
90 | {
91 | return false;
92 | }
93 |
94 | return true;
95 | }
96 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Connectors/LGConnector.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.AI;
2 |
3 | using OllamaSharp;
4 |
5 | using OpenChat.PlaygroundApp.Abstractions;
6 | using OpenChat.PlaygroundApp.Configurations;
7 |
8 | namespace OpenChat.PlaygroundApp.Connectors;
9 |
10 | ///
11 | /// This represents the connector entity for LG AI EXAONE.
12 | ///
13 | public class LGConnector(AppSettings settings) : LanguageModelConnector(settings.LG)
14 | {
15 | private readonly AppSettings _appSettings = settings ?? throw new ArgumentNullException(nameof(settings));
16 |
17 | ///
18 | public override bool EnsureLanguageModelSettingsValid()
19 | {
20 | if (this.Settings is not LGSettings settings)
21 | {
22 | throw new InvalidOperationException("Missing configuration: LG.");
23 | }
24 |
25 | if (string.IsNullOrWhiteSpace(settings.BaseUrl!.Trim()) == true)
26 | {
27 | throw new InvalidOperationException("Missing configuration: LG:BaseUrl.");
28 | }
29 |
30 | if (string.IsNullOrWhiteSpace(settings.Model!.Trim()) == true)
31 | {
32 | throw new InvalidOperationException("Missing configuration: LG:Model.");
33 | }
34 |
35 | if (IsValidModel(settings.Model.Trim()) == false)
36 | {
37 | throw new InvalidOperationException("Invalid configuration: Expected 'hf.co/LGAI-EXAONE/EXAONE-*-GGUF' format.");
38 | }
39 |
40 | return true;
41 | }
42 |
43 | ///
44 | public override async Task GetChatClientAsync()
45 | {
46 | var settings = this.Settings as LGSettings;
47 | var baseUrl = settings!.BaseUrl!;
48 | var model = settings!.Model!;
49 |
50 | var config = new OllamaApiClient.Configuration
51 | {
52 | Uri = new Uri(baseUrl),
53 | Model = model,
54 | };
55 |
56 | var chatClient = new OllamaApiClient(config);
57 |
58 | var pulls = chatClient.PullModelAsync(model);
59 | await foreach (var pull in pulls)
60 | {
61 | Console.WriteLine($"Pull status: {pull!.Status}");
62 | }
63 |
64 | Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {settings.Model}");
65 |
66 | return await Task.FromResult(chatClient).ConfigureAwait(false);
67 | }
68 |
69 | private static bool IsValidModel(string model)
70 | {
71 | var segments = model.Split(['/'], StringSplitOptions.RemoveEmptyEntries);
72 |
73 | if (segments.Length != 3)
74 | {
75 | return false;
76 | }
77 |
78 | if (segments[0].Equals("hf.co", StringComparison.InvariantCultureIgnoreCase) == false)
79 | {
80 | return false;
81 | }
82 |
83 | if (segments[1].Equals("LGAI-EXAONE", StringComparison.InvariantCultureIgnoreCase) == false)
84 | {
85 | return false;
86 | }
87 |
88 | if (segments[2].StartsWith("EXAONE-", StringComparison.InvariantCultureIgnoreCase) == false)
89 | {
90 | return false;
91 | }
92 |
93 | if (segments[2].EndsWith("-GGUF", StringComparison.InvariantCultureIgnoreCase) == false)
94 | {
95 | return false;
96 | }
97 |
98 | return true;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Connectors/OpenAIConnector.cs:
--------------------------------------------------------------------------------
1 | using System.ClientModel;
2 |
3 | using Microsoft.Extensions.AI;
4 |
5 | using OpenAI;
6 |
7 | using OpenChat.PlaygroundApp.Abstractions;
8 | using OpenChat.PlaygroundApp.Configurations;
9 |
10 | namespace OpenChat.PlaygroundApp.Connectors;
11 |
12 | ///
13 | /// This represents the connector entity for OpenAI.
14 | ///
15 | /// instance.
16 | public class OpenAIConnector(AppSettings settings) : LanguageModelConnector(settings.OpenAI)
17 | {
18 | private readonly AppSettings _appSettings = settings ?? throw new ArgumentNullException(nameof(settings));
19 |
20 | ///
21 | public override bool EnsureLanguageModelSettingsValid()
22 | {
23 | if (this.Settings is not OpenAISettings settings)
24 | {
25 | throw new InvalidOperationException("Missing configuration: OpenAI.");
26 | }
27 |
28 | if (string.IsNullOrWhiteSpace(settings.ApiKey!.Trim()) == true)
29 | {
30 | throw new InvalidOperationException("Missing configuration: OpenAI:ApiKey.");
31 | }
32 |
33 | if (string.IsNullOrWhiteSpace(settings.Model!.Trim()) == true)
34 | {
35 | throw new InvalidOperationException("Missing configuration: OpenAI:Model.");
36 | }
37 |
38 | return true;
39 | }
40 |
41 | ///
42 | public override async Task GetChatClientAsync()
43 | {
44 | var settings = this.Settings as OpenAISettings;
45 |
46 | var credential = new ApiKeyCredential(settings?.ApiKey ?? throw new InvalidOperationException("Missing configuration: OpenAI:ApiKey."));
47 |
48 | var client = new OpenAIClient(credential);
49 | var chatClient = client.GetChatClient(settings.Model)
50 | .AsIChatClient();
51 |
52 | Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {settings.Model}");
53 |
54 | return await Task.FromResult(chatClient).ConfigureAwait(false);
55 | }
56 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Connectors/UpstageConnector.cs:
--------------------------------------------------------------------------------
1 | using System.ClientModel;
2 |
3 | using Microsoft.Extensions.AI;
4 |
5 | using OpenAI;
6 |
7 | using OpenChat.PlaygroundApp.Abstractions;
8 | using OpenChat.PlaygroundApp.Configurations;
9 |
10 | namespace OpenChat.PlaygroundApp.Connectors;
11 |
12 | ///
13 | /// This represents the connector entity for Upstage.
14 | ///
15 | public class UpstageConnector(AppSettings settings) : LanguageModelConnector(settings.Upstage)
16 | {
17 | ///
18 | public override bool EnsureLanguageModelSettingsValid()
19 | {
20 | var settings = this.Settings as UpstageSettings;
21 | if (settings is null)
22 | {
23 | throw new InvalidOperationException("Missing configuration: Upstage.");
24 | }
25 |
26 | if (string.IsNullOrWhiteSpace(settings.BaseUrl?.Trim()) == true)
27 | {
28 | throw new InvalidOperationException("Missing configuration: Upstage:BaseUrl.");
29 | }
30 |
31 | if (string.IsNullOrWhiteSpace(settings.ApiKey?.Trim()) == true)
32 | {
33 | throw new InvalidOperationException("Missing configuration: Upstage:ApiKey.");
34 | }
35 |
36 | if (string.IsNullOrWhiteSpace(settings.Model?.Trim()) == true)
37 | {
38 | throw new InvalidOperationException("Missing configuration: Upstage:Model.");
39 | }
40 |
41 | return true;
42 | }
43 |
44 | ///
45 | public override async Task GetChatClientAsync()
46 | {
47 | var settings = this.Settings as UpstageSettings;
48 |
49 | var credential = new ApiKeyCredential(settings?.ApiKey ??
50 | throw new InvalidOperationException("Missing configuration: Upstage:ApiKey."));
51 |
52 | var options = new OpenAIClientOptions
53 | {
54 | Endpoint = new Uri(settings.BaseUrl ??
55 | throw new InvalidOperationException("Missing configuration: Upstage:BaseUrl."))
56 | };
57 |
58 | var client = new OpenAIClient(credential, options);
59 | var chatClient = client.GetChatClient(settings.Model)
60 | .AsIChatClient();
61 |
62 | return await Task.FromResult(chatClient).ConfigureAwait(false);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Constants/AppSettingConstants.cs:
--------------------------------------------------------------------------------
1 | namespace OpenChat.PlaygroundApp.Constants;
2 |
3 | ///
4 | /// This represents the app settings argument constants for all app settings arguments to reference.
5 | ///
6 | public static class AppSettingConstants
7 | {
8 | ///
9 | /// Defines the constant for 'ConnectorType'.
10 | ///
11 | public const string ConnectorType = "ConnectorType";
12 | }
13 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Endpoints/ChatResponseEndpoint.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | using Microsoft.AspNetCore.Mvc;
4 | using Microsoft.Extensions.AI;
5 |
6 | using OpenChat.PlaygroundApp.Models;
7 | using OpenChat.PlaygroundApp.Services;
8 |
9 | using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
10 | using ChatResponse = OpenChat.PlaygroundApp.Models.ChatResponse;
11 |
12 | namespace OpenChat.PlaygroundApp.Endpoints;
13 |
14 | ///
15 | /// This represents the endpoint entity for chat operations.
16 | ///
17 | /// The .
18 | /// The .
19 | public class ChatResponseEndpoint(IChatService chatService, ILogger logger) : IEndpoint
20 | {
21 | private readonly IChatService _chatService = chatService ?? throw new ArgumentNullException(nameof(chatService));
22 | private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
23 |
24 | ///
25 | public void MapEndpoint(IEndpointRouteBuilder app)
26 | {
27 | app.MapPost("/chat/responses", PostChatResponseAsync)
28 | .WithTags("Chat")
29 | .Accepts>(contentType: "application/json")
30 | .Produces>(statusCode: StatusCodes.Status200OK, contentType: "application/json")
31 | .WithName("PostChatResponses")
32 | .WithOpenApi();
33 | }
34 |
35 | private async IAsyncEnumerable PostChatResponseAsync(
36 | [FromBody] IEnumerable request,
37 | [EnumeratorCancellation] CancellationToken cancellationToken)
38 | {
39 | var chats = request.ToList();
40 |
41 | this._logger.LogInformation("Received {RequestCount} chat requests", chats.Count);
42 |
43 | var messages = chats.Select(chat => new ChatMessage(new(chat.Role), chat.Message));
44 | var options = new ChatOptions();
45 |
46 | var result = this._chatService.GetStreamingResponseAsync(messages, options, cancellationToken: cancellationToken);
47 | await foreach (var update in result)
48 | {
49 | yield return new ChatResponse { Role = update.Role?.Value ?? string.Empty, Message = update.Text };
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Endpoints/EndpointExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 |
3 | using Microsoft.Extensions.DependencyInjection.Extensions;
4 |
5 | namespace OpenChat.PlaygroundApp.Endpoints;
6 |
7 | ///
8 | /// This represents the extension entity for handling endpoints.
9 | ///
10 | public static class EndpointExtensions
11 | {
12 | ///
13 | /// Adds the chat client to the service collection.
14 | ///
15 | /// The instance.
16 | /// The instance.
17 | /// Returns the modified instance.
18 | public static IServiceCollection AddEndpoints(this IServiceCollection services, Assembly assembly)
19 | {
20 | var descriptors = assembly.DefinedTypes
21 | .Where(type => type is { IsAbstract: false, IsInterface: false }
22 | && type.IsAssignableTo(typeof(IEndpoint)))
23 | .Select(type => ServiceDescriptor.Scoped(typeof(IEndpoint), type));
24 | services.TryAddEnumerable(descriptors);
25 |
26 | return services;
27 | }
28 |
29 | ///
30 | /// Maps all the registered endpoints.
31 | ///
32 | /// instance.
33 | /// instance.
34 | /// Returns instance.
35 | public static IApplicationBuilder MapEndpoints(this WebApplication app, RouteGroupBuilder? group = default)
36 | {
37 | IEndpointRouteBuilder builder = group is null ? app : group;
38 |
39 | var endpoints = app.Services
40 | .GetRequiredService()
41 | .CreateScope()
42 | .ServiceProvider
43 | .GetRequiredService>();
44 | foreach (var endpoint in endpoints)
45 | {
46 | endpoint.MapEndpoint(builder);
47 | }
48 |
49 | return app;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Endpoints/IEndpoint.cs:
--------------------------------------------------------------------------------
1 | namespace OpenChat.PlaygroundApp.Endpoints;
2 |
3 | ///
4 | /// This provides interfaces to the endpoints.
5 | ///
6 | public interface IEndpoint
7 | {
8 | ///
9 | /// Maps the endpoint to the specified .
10 | ///
11 | /// instance.
12 | void MapEndpoint(IEndpointRouteBuilder app);
13 | }
14 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Models/ChatRequest.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace OpenChat.PlaygroundApp.Models;
4 |
5 | ///
6 | /// This represents the chat request entity.
7 | ///
8 | public class ChatRequest
9 | {
10 | ///
11 | /// Gets or sets the role of the message sender.
12 | ///
13 | [Required]
14 | public string Role { get; set; } = string.Empty;
15 |
16 | ///
17 | /// Gets or sets the message content.
18 | ///
19 | [Required]
20 | public string Message { get; set; } = string.Empty;
21 | }
22 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Models/ChatResponse.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace OpenChat.PlaygroundApp.Models;
4 |
5 | ///
6 | /// This represents the chat response entity.
7 | ///
8 | public class ChatResponse
9 | {
10 | ///
11 | /// Gets or sets the role of the message content.
12 | ///
13 | public string Role { get; set; } = string.Empty;
14 |
15 | ///
16 | /// Gets or sets the message content.
17 | ///
18 | [Required]
19 | public string Message { get; set; } = string.Empty;
20 | }
21 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/OpenApi/OpenApiDocumentTransformer.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.OpenApi;
2 | using Microsoft.OpenApi.Models;
3 |
4 | namespace OpenChat.PlaygroundApp.OpenApi;
5 |
6 | ///
7 | /// This represents the transformer entity for OpenAPI document.
8 | ///
9 | public class OpenApiDocumentTransformer(IHttpContextAccessor accessor) : IOpenApiDocumentTransformer
10 | {
11 | ///
12 | public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
13 | {
14 | document.Info = new OpenApiInfo
15 | {
16 | Title = "OpenChat Playground API",
17 | Version = "1.0.0",
18 | Description = "An API for the OpenChat Playground."
19 | };
20 | document.Servers =
21 | [
22 | new OpenApiServer
23 | {
24 | Url = accessor.HttpContext != null
25 | ? $"{accessor.HttpContext.Request.Scheme}://{accessor.HttpContext.Request.Host}/"
26 | : "http://localhost:5280/"
27 | }
28 | ];
29 |
30 | return Task.CompletedTask;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/OpenChat.PlaygroundApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | latest
6 |
7 | enable
8 | enable
9 |
10 | OpenChat.PlaygroundApp
11 | OpenChat.PlaygroundApp
12 |
13 | 6bf996dc-c60b-4c4e-b34f-0e06358687bf
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/OpenChat.PlaygroundApp.http:
--------------------------------------------------------------------------------
1 | @OpenChat.PlaygroundApp_HostAddress = http://localhost:5280
2 |
3 | POST {{OpenChat.PlaygroundApp_HostAddress}}/api/chat/responses
4 | Accept: application/json
5 | Content-Type: application/json
6 |
7 | [
8 | {
9 | "role": "system",
10 | "message": "You're a helpful assistant."
11 | },
12 | {
13 | "role": "user",
14 | "message": "Why is the sky blue?"
15 | }
16 | ]
17 |
18 | ###
19 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Options/AmazonBedrockArgumentOptions.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Configurations;
3 | using OpenChat.PlaygroundApp.Constants;
4 |
5 | namespace OpenChat.PlaygroundApp.Options;
6 |
7 | ///
8 | /// This represents the argument options entity for Amazon Bedrock.
9 | ///
10 | public class AmazonBedrockArgumentOptions : ArgumentOptions
11 | {
12 | ///
13 | /// Gets or sets the AWSCredentials Access Key ID for the Amazon Bedrock service.
14 | ///
15 | public string? AccessKeyId { get; set; }
16 |
17 | ///
18 | /// Gets or sets the AWSCredentials Secret Access Key for the Amazon Bedrock service.
19 | ///
20 | public string? SecretAccessKey { get; set; }
21 |
22 | ///
23 | /// Gets or sets the AWS region for the Amazon Bedrock service.
24 | ///
25 | public string? Region { get; set; }
26 |
27 | ///
28 | /// Gets or sets the model for the Amazon Bedrock service.
29 | ///
30 | public string? ModelId { get; set; }
31 |
32 | ///
33 | protected override void ParseOptions(IConfiguration config, string[] args)
34 | {
35 | var settings = new AppSettings();
36 | config.Bind(settings);
37 |
38 | var amazonBedrock = settings.AmazonBedrock;
39 |
40 | this.AccessKeyId ??= amazonBedrock?.AccessKeyId;
41 | this.SecretAccessKey ??= amazonBedrock?.SecretAccessKey;
42 | this.Region ??= amazonBedrock?.Region;
43 | this.ModelId ??= amazonBedrock?.ModelId;
44 |
45 | for (var i = 0; i < args.Length; i++)
46 | {
47 | switch (args[i])
48 | {
49 | case ArgumentOptionConstants.AmazonBedrock.AccessKeyId:
50 | if (i + 1 < args.Length)
51 | {
52 | this.AccessKeyId = args[++i];
53 | }
54 | break;
55 |
56 | case ArgumentOptionConstants.AmazonBedrock.SecretAccessKey:
57 | if (i + 1 < args.Length)
58 | {
59 | this.SecretAccessKey = args[++i];
60 | }
61 | break;
62 |
63 | case ArgumentOptionConstants.AmazonBedrock.Region:
64 | if (i + 1 < args.Length)
65 | {
66 | this.Region = args[++i];
67 | }
68 | break;
69 |
70 | case ArgumentOptionConstants.AmazonBedrock.ModelId:
71 | if (i + 1 < args.Length)
72 | {
73 | this.ModelId = args[++i];
74 | }
75 | break;
76 |
77 | default:
78 | break;
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Options/AnthropicArgumentOptions.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Configurations;
3 | using OpenChat.PlaygroundApp.Constants;
4 |
5 | namespace OpenChat.PlaygroundApp.Options;
6 |
7 | ///
8 | /// This represents the argument options entity for Anthropic Claude.
9 | ///
10 | public class AnthropicArgumentOptions : ArgumentOptions
11 | {
12 | ///
13 | /// Gets or sets the API key for Anthropic Claude.
14 | ///
15 | public string? ApiKey { get; set; }
16 |
17 | ///
18 | /// Gets or sets the model name of Anthropic Claude.
19 | ///
20 | public string? Model { get; set; }
21 |
22 | ///
23 | protected override void ParseOptions(IConfiguration config, string[] args)
24 | {
25 | var settings = new AppSettings();
26 | config.Bind(settings);
27 |
28 | var anthropic = settings.Anthropic;
29 |
30 | this.ApiKey ??= anthropic?.ApiKey;
31 | this.Model ??= anthropic?.Model;
32 |
33 | for (var i = 0; i < args.Length; i++)
34 | {
35 | switch (args[i])
36 | {
37 | case ArgumentOptionConstants.Anthropic.ApiKey:
38 | if (i + 1 < args.Length)
39 | {
40 | this.ApiKey = args[++i];
41 | }
42 | break;
43 |
44 | case ArgumentOptionConstants.Anthropic.Model:
45 | if (i + 1 < args.Length)
46 | {
47 | this.Model = args[++i];
48 | }
49 | break;
50 |
51 | default:
52 | break;
53 | }
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Options/AzureAIFoundryArgumentOptions.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Configurations;
3 | using OpenChat.PlaygroundApp.Constants;
4 |
5 | namespace OpenChat.PlaygroundApp.Options;
6 |
7 | ///
8 | /// This represents the argument options entity for Azure AI Foundry.
9 | ///
10 | public class AzureAIFoundryArgumentOptions : ArgumentOptions
11 | {
12 | ///
13 | /// Gets or sets the endpoint URL for Azure AI Foundry API.
14 | ///
15 | public string? Endpoint { get; set; }
16 |
17 | ///
18 | /// Gets or sets the personal access token for Azure AI Foundry.
19 | ///
20 | public string? ApiKey { get; set; }
21 |
22 | ///
23 | /// Gets or sets the model name of Azure AI Foundry.
24 | ///
25 | public string? DeploymentName { get; set; }
26 |
27 | ///
28 | protected override void ParseOptions(IConfiguration config, string[] args)
29 | {
30 | var settings = new AppSettings();
31 | config.Bind(settings);
32 |
33 | var azureAIFoundry = settings.AzureAIFoundry;
34 |
35 | this.Endpoint ??= azureAIFoundry?.Endpoint;
36 | this.ApiKey ??= azureAIFoundry?.ApiKey;
37 | this.DeploymentName ??= azureAIFoundry?.DeploymentName;
38 |
39 | for (var i = 0; i < args.Length; i++)
40 | {
41 | switch (args[i])
42 | {
43 | case ArgumentOptionConstants.AzureAIFoundry.Endpoint:
44 | if (i + 1 < args.Length)
45 | {
46 | this.Endpoint = args[++i];
47 | }
48 | break;
49 |
50 | case ArgumentOptionConstants.AzureAIFoundry.ApiKey:
51 | if (i + 1 < args.Length)
52 | {
53 | this.ApiKey = args[++i];
54 | }
55 | break;
56 |
57 | case ArgumentOptionConstants.AzureAIFoundry.DeploymentName:
58 | if (i + 1 < args.Length)
59 | {
60 | this.DeploymentName = args[++i];
61 | }
62 | break;
63 |
64 | default:
65 | break;
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Options/DockerModelRunnerArgumentOptions.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 |
3 | namespace OpenChat.PlaygroundApp.Options;
4 |
5 | ///
6 | /// This represents the argument options entity for Docker Model Runner.
7 | ///
8 | public class DockerModelRunnerArgumentOptions : ArgumentOptions
9 | {
10 | ///
11 | /// Gets or sets the Docker Model Runner Base URL.
12 | ///
13 | public string? BaseUrl { get; set; }
14 |
15 | ///
16 | /// Gets or sets the Docker Model Runner model/deployment name.
17 | ///
18 | public string? Model { get; set; }
19 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Options/FoundryLocalArgumentOptions.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Configurations;
3 | using OpenChat.PlaygroundApp.Constants;
4 |
5 | namespace OpenChat.PlaygroundApp.Options;
6 |
7 | ///
8 | /// This represents the argument options entity for Foundry Local.
9 | ///
10 | public class FoundryLocalArgumentOptions : ArgumentOptions
11 | {
12 | ///
13 | /// Gets or sets the alias of Foundry Local.
14 | ///
15 | public string? Alias { get; set; }
16 |
17 | ///
18 | protected override void ParseOptions(IConfiguration config, string[] args)
19 | {
20 | var settings = new AppSettings();
21 | config.Bind(settings);
22 |
23 | var foundryLocal = settings.FoundryLocal;
24 |
25 | this.Alias ??= foundryLocal?.Alias;
26 |
27 | for (var i = 0; i < args.Length; i++)
28 | {
29 | switch (args[i])
30 | {
31 | case ArgumentOptionConstants.FoundryLocal.Alias:
32 | if (i + 1 < args.Length)
33 | {
34 | this.Alias = args[++i];
35 | }
36 | break;
37 |
38 | default:
39 | break;
40 | }
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Options/GitHubModelsArgumentOptions.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Configurations;
3 | using OpenChat.PlaygroundApp.Constants;
4 |
5 | namespace OpenChat.PlaygroundApp.Options;
6 |
7 | ///
8 | /// This represents the argument options entity for GitHub Models.
9 | ///
10 | public class GitHubModelsArgumentOptions : ArgumentOptions
11 | {
12 | ///
13 | /// Gets or sets the endpoint URL for GitHub Models API.
14 | ///
15 | public string? Endpoint { get; set; }
16 |
17 | ///
18 | /// Gets or sets the personal access token for GitHub Models.
19 | ///
20 | public string? Token { get; set; }
21 |
22 | ///
23 | /// Gets or sets the model name of GitHub Models.
24 | ///
25 | public string? Model { get; set; }
26 |
27 | ///
28 | protected override void ParseOptions(IConfiguration config, string[] args)
29 | {
30 | var settings = new AppSettings();
31 | config.Bind(settings);
32 |
33 | var github = settings.GitHubModels;
34 |
35 | this.Endpoint ??= github?.Endpoint;
36 | this.Token ??= github?.Token;
37 | this.Model ??= github?.Model;
38 |
39 | for (var i = 0; i < args.Length; i++)
40 | {
41 | switch (args[i])
42 | {
43 | case ArgumentOptionConstants.GitHubModels.Endpoint:
44 | if (i + 1 < args.Length)
45 | {
46 | this.Endpoint = args[++i];
47 | }
48 | break;
49 |
50 | case ArgumentOptionConstants.GitHubModels.Token:
51 | if (i + 1 < args.Length)
52 | {
53 | this.Token = args[++i];
54 | }
55 | break;
56 |
57 | case ArgumentOptionConstants.GitHubModels.Model:
58 | if (i + 1 < args.Length)
59 | {
60 | this.Model = args[++i];
61 | }
62 | break;
63 |
64 | default:
65 | break;
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Options/GoogleVertexAIArgumentOptions.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Configurations;
3 | using OpenChat.PlaygroundApp.Constants;
4 |
5 | namespace OpenChat.PlaygroundApp.Options;
6 |
7 | ///
8 | /// This represents the argument options entity for Google Vertex AI.
9 | ///
10 | public class GoogleVertexAIArgumentOptions : ArgumentOptions
11 | {
12 | ///
13 | /// Gets or sets the Google Vertex AI API Key.
14 | ///
15 | public string? ApiKey { get; set; }
16 |
17 | ///
18 | /// Gets or sets the model name of Google Vertex AI.
19 | ///
20 | public string? Model { get; set; }
21 |
22 |
23 | ///
24 | protected override void ParseOptions(IConfiguration config, string[] args)
25 | {
26 | var settings = new AppSettings();
27 | config.Bind(settings);
28 |
29 | var googleVertexAI = settings.GoogleVertexAI;
30 |
31 | this.ApiKey ??= googleVertexAI?.ApiKey;
32 | this.Model ??= googleVertexAI?.Model;
33 |
34 | for (var i = 0; i < args.Length; i++)
35 | {
36 | switch (args[i])
37 | {
38 | case ArgumentOptionConstants.GoogleVertexAI.ApiKey:
39 | if (i + 1 < args.Length)
40 | {
41 | this.ApiKey = args[++i];
42 | }
43 | break;
44 |
45 | case ArgumentOptionConstants.GoogleVertexAI.Model:
46 | if (i + 1 < args.Length)
47 | {
48 | this.Model = args[++i];
49 | }
50 | break;
51 |
52 | default:
53 | break;
54 | }
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Options/HuggingFaceArgumentOptions.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Configurations;
3 | using OpenChat.PlaygroundApp.Constants;
4 |
5 | namespace OpenChat.PlaygroundApp.Options;
6 |
7 | ///
8 | /// This represents the argument options entity for Hugging Face.
9 | ///
10 | public class HuggingFaceArgumentOptions : ArgumentOptions
11 | {
12 | ///
13 | /// Gets or sets the base URL for the Hugging Face API.
14 | ///
15 | public string? BaseUrl { get; set; }
16 |
17 | ///
18 | /// Gets or sets the model name for Hugging Face.
19 | ///
20 | public string? Model { get; set; }
21 |
22 | ///
23 | protected override void ParseOptions(IConfiguration config, string[] args)
24 | {
25 | var settings = new AppSettings();
26 | config.Bind(settings);
27 |
28 | var huggingFace = settings.HuggingFace;
29 |
30 | this.BaseUrl ??= huggingFace?.BaseUrl;
31 | this.Model ??= huggingFace?.Model;
32 |
33 | for (var i = 0; i < args.Length; i++)
34 | {
35 | switch (args[i])
36 | {
37 | case ArgumentOptionConstants.HuggingFace.BaseUrl:
38 | if (i + 1 < args.Length)
39 | {
40 | this.BaseUrl = args[++i];
41 | }
42 | break;
43 |
44 | case ArgumentOptionConstants.HuggingFace.Model:
45 | if (i + 1 < args.Length)
46 | {
47 | this.Model = args[++i];
48 | }
49 | break;
50 |
51 | default:
52 | break;
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Options/LGArgumentOptions.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Configurations;
3 | using OpenChat.PlaygroundApp.Constants;
4 |
5 | namespace OpenChat.PlaygroundApp.Options;
6 |
7 | ///
8 | /// Represents the command-line argument options for LG AI EXAONE.
9 | ///
10 | public class LGArgumentOptions : ArgumentOptions
11 | {
12 | ///
13 | /// Gets or sets the base URL for LG AI EXAONE API.
14 | ///
15 | public string? BaseUrl { get; set; }
16 |
17 | ///
18 | /// Gets or sets the model name for LG AI EXAONE.
19 | ///
20 | public string? Model { get; set; }
21 |
22 | protected override void ParseOptions(IConfiguration config, string[] args)
23 | {
24 | var settings = new AppSettings();
25 | config.Bind(settings);
26 |
27 | var lg = settings.LG;
28 |
29 | this.BaseUrl ??= lg?.BaseUrl;
30 | this.Model ??= lg?.Model;
31 |
32 | for (var i = 0; i < args.Length; i++)
33 | {
34 | switch (args[i])
35 | {
36 | case ArgumentOptionConstants.LG.BaseUrl:
37 | if (i + 1 < args.Length)
38 | {
39 | this.BaseUrl = args[++i];
40 | }
41 | break;
42 |
43 | case ArgumentOptionConstants.LG.Model:
44 | if (i + 1 < args.Length)
45 | {
46 | this.Model = args[++i];
47 | }
48 | break;
49 |
50 | default:
51 | break;
52 | }
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Options/OllamaArgumentOptions.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Configurations;
3 | using OpenChat.PlaygroundApp.Constants;
4 |
5 | namespace OpenChat.PlaygroundApp.Options;
6 |
7 |
8 | ///
9 | /// Represents the command-line argument options for Ollama.
10 | ///
11 | public class OllamaArgumentOptions : ArgumentOptions
12 | {
13 | ///
14 | /// Gets or sets the base URL for Ollama API.
15 | ///
16 | public string? BaseUrl { get; set; }
17 |
18 | ///
19 | /// Gets or sets the model name for Ollama.
20 | ///
21 | public string? Model { get; set; }
22 |
23 | protected override void ParseOptions(IConfiguration config, string[] args)
24 | {
25 | var settings = new AppSettings();
26 | config.Bind(settings);
27 |
28 | var ollama = settings.Ollama;
29 |
30 | this.BaseUrl ??= ollama?.BaseUrl;
31 | this.Model ??= ollama?.Model;
32 |
33 | for (var i = 0; i < args.Length; i++)
34 | {
35 | switch (args[i])
36 | {
37 | case ArgumentOptionConstants.Ollama.BaseUrl:
38 | if (i + 1 < args.Length)
39 | {
40 | this.BaseUrl = args[++i];
41 | }
42 | break;
43 | case ArgumentOptionConstants.Ollama.Model:
44 | if (i + 1 < args.Length)
45 | {
46 | this.Model = args[++i];
47 | }
48 | break;
49 | default:
50 | break;
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Options/OpenAIArgumentOptions.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Configurations;
3 | using OpenChat.PlaygroundApp.Constants;
4 |
5 | namespace OpenChat.PlaygroundApp.Options;
6 |
7 | ///
8 | /// This represents the argument options entity for OpenAI.
9 | ///
10 | public class OpenAIArgumentOptions : ArgumentOptions
11 | {
12 | ///
13 | /// Gets or sets the OpenAI API key.
14 | ///
15 | public string? ApiKey { get; set; }
16 |
17 | ///
18 | /// Gets or sets the OpenAI model name.
19 | ///
20 | public string? Model { get; set; }
21 |
22 | ///
23 | protected override void ParseOptions(IConfiguration config, string[] args)
24 | {
25 | var settings = new AppSettings();
26 | config.Bind(settings);
27 |
28 | var openai = settings.OpenAI;
29 |
30 | this.ApiKey ??= openai?.ApiKey;
31 | this.Model ??= openai?.Model;
32 |
33 | for (var i = 0; i < args.Length; i++)
34 | {
35 | switch (args[i])
36 | {
37 | case ArgumentOptionConstants.OpenAI.ApiKey:
38 | if (i + 1 < args.Length)
39 | {
40 | this.ApiKey = args[++i];
41 | }
42 | break;
43 |
44 | case ArgumentOptionConstants.OpenAI.Model:
45 | if (i + 1 < args.Length)
46 | {
47 | this.Model = args[++i];
48 | }
49 | break;
50 |
51 | default:
52 | break;
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Options/UpstageArgumentOptions.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Configurations;
3 | using OpenChat.PlaygroundApp.Constants;
4 |
5 | namespace OpenChat.PlaygroundApp.Options;
6 |
7 | ///
8 | /// This represents the argument options entity for Upstage.
9 | ///
10 | public class UpstageArgumentOptions : ArgumentOptions
11 | {
12 | ///
13 | /// Gets or sets the base URL for Upstage API.
14 | ///
15 | public string? BaseUrl { get; set; }
16 |
17 | ///
18 | /// Gets or sets the API key for Upstage.
19 | ///
20 | public string? ApiKey { get; set; }
21 |
22 | ///
23 | /// Gets or sets the model name of Upstage.
24 | ///
25 | public string? Model { get; set; }
26 |
27 | ///
28 | protected override void ParseOptions(IConfiguration config, string[] args)
29 | {
30 | var settings = new AppSettings();
31 | config.Bind(settings);
32 |
33 | var upstage = settings.Upstage;
34 |
35 | this.BaseUrl ??= upstage?.BaseUrl;
36 | this.ApiKey ??= upstage?.ApiKey;
37 | this.Model ??= upstage?.Model;
38 |
39 | for (var i = 0; i < args.Length; i++)
40 | {
41 | switch (args[i])
42 | {
43 | case ArgumentOptionConstants.Upstage.BaseUrl:
44 | if (i + 1 < args.Length)
45 | {
46 | this.BaseUrl = args[++i];
47 | }
48 | break;
49 |
50 | case ArgumentOptionConstants.Upstage.ApiKey:
51 | if (i + 1 < args.Length)
52 | {
53 | this.ApiKey = args[++i];
54 | }
55 | break;
56 |
57 | case ArgumentOptionConstants.Upstage.Model:
58 | if (i + 1 < args.Length)
59 | {
60 | this.Model = args[++i];
61 | }
62 | break;
63 |
64 | default:
65 | break;
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.AI;
2 |
3 | using OpenChat.PlaygroundApp.Abstractions;
4 | using OpenChat.PlaygroundApp.Components;
5 | using OpenChat.PlaygroundApp.Endpoints;
6 | using OpenChat.PlaygroundApp.OpenApi;
7 | using OpenChat.PlaygroundApp.Services;
8 |
9 | var builder = WebApplication.CreateBuilder(args);
10 |
11 | var config = builder.Configuration;
12 | var settings = ArgumentOptions.Parse(config, args);
13 | if (settings.Help == true)
14 | {
15 | ArgumentOptions.DisplayHelp();
16 | return;
17 | }
18 |
19 | builder.Services.AddSingleton(settings!);
20 |
21 | builder.Services.AddRazorComponents()
22 | .AddInteractiveServerComponents();
23 |
24 | var chatClient = await LanguageModelConnector.CreateChatClientAsync(settings);
25 |
26 | builder.Services.AddChatClient(chatClient)
27 | .UseFunctionInvocation()
28 | .UseLogging();
29 |
30 | builder.Services.AddHttpContextAccessor();
31 | builder.Services.AddOpenApi("openapi", options =>
32 | {
33 | options.AddDocumentTransformer();
34 | });
35 |
36 | builder.Services.AddScoped();
37 | builder.Services.AddEndpoints(typeof(Program).Assembly);
38 |
39 | var app = builder.Build();
40 |
41 | // Configure the HTTP request pipeline.
42 | if (!app.Environment.IsDevelopment())
43 | {
44 | app.UseExceptionHandler("/Error", createScopeForErrors: true);
45 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
46 | app.UseHsts();
47 |
48 | app.UseHttpsRedirection();
49 | }
50 |
51 | app.UseAntiforgery();
52 | app.UseStaticFiles();
53 |
54 | if (app.Environment.IsDevelopment())
55 | {
56 | app.MapOpenApi("/{documentName}.json");
57 | }
58 |
59 | var group = app.MapGroup("/api");
60 | app.MapEndpoints(group);
61 |
62 | app.MapRazorComponents()
63 | .AddInteractiveServerRenderMode();
64 |
65 | await app.RunAsync();
66 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "http": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": true,
8 | "applicationUrl": "http://localhost:5280",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development"
11 | }
12 | },
13 | "https": {
14 | "commandName": "Project",
15 | "dotnetRunMessages": true,
16 | "launchBrowser": true,
17 | "applicationUrl": "https://localhost:45280;http://localhost:5280",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/Services/ChatService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.AI;
2 |
3 | namespace OpenChat.PlaygroundApp.Services;
4 |
5 | ///
6 | /// This provides interfaces to the chat service.
7 | ///
8 | public interface IChatService
9 | {
10 | ///
11 | /// Sends chat messages and streams the response.
12 | ///
13 | /// The sequence of to send.
14 | /// The with which to configure the request.
15 | /// The to monitor for cancellation requests. The default is .
16 | /// The generated.
17 | IAsyncEnumerable GetStreamingResponseAsync(
18 | IEnumerable messages,
19 | ChatOptions? options = null,
20 | CancellationToken cancellationToken = default);
21 | }
22 |
23 | ///
24 | /// This represents the service entity for chat operations.
25 | ///
26 | /// The .
27 | /// The .
28 | public class ChatService(IChatClient chatClient, ILogger logger) : IChatService
29 | {
30 | private readonly IChatClient _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient));
31 | private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
32 |
33 | ///
34 | public IAsyncEnumerable GetStreamingResponseAsync(
35 | IEnumerable messages,
36 | ChatOptions? options = null,
37 | CancellationToken cancellationToken = default)
38 | {
39 | var chats = messages.ToList();
40 | if (chats.Count < 2)
41 | {
42 | throw new ArgumentException("At least two messages are required", nameof(messages));
43 | }
44 |
45 | if (chats.First().Role != ChatRole.System)
46 | {
47 | throw new ArgumentException("The first message must be a system message", nameof(messages));
48 | }
49 |
50 | if (chats.ElementAt(1).Role != ChatRole.User)
51 | {
52 | throw new ArgumentException("The second message must be a user message", nameof(messages));
53 | }
54 |
55 | this._logger.LogInformation("Requesting chat response with {MessageCount} messages", chats.Count);
56 |
57 | return this._chatClient.GetStreamingResponseAsync(chats, options, cancellationToken);
58 | }
59 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning",
6 | "Microsoft.EntityFrameworkCore": "Warning"
7 | }
8 | },
9 |
10 | "AllowedHosts": "*",
11 |
12 | "ConnectorType": "GitHubModels",
13 |
14 | "AmazonBedrock": {
15 | "AccessKeyId": "{{AWS_ACCESS_KEY_ID}}",
16 | "SecretAccessKey": "{{AWS_SECRET_ACCESS_KEY}}",
17 | "Region": "{{AWS_REGION}}",
18 | "ModelId": "anthropic.claude-sonnet-4-20250514-v1:0"
19 | },
20 |
21 | "AzureAIFoundry": {
22 | "Endpoint": "{{AZURE_ENDPOINT}}",
23 | "ApiKey": "{{AZURE_API_KEY}}",
24 | "DeploymentName": "gpt-4o-mini"
25 | },
26 |
27 | "GitHubModels": {
28 | "Endpoint": "https://models.github.ai/inference",
29 | "Token": "{{GITHUB_PAT}}",
30 | "Model": "openai/gpt-4o-mini"
31 | },
32 |
33 | "GoogleVertexAI": {
34 | "ApiKey": "{{GOOGLE_API_KEY}}",
35 | "Model": "gemini-2.5-flash-lite"
36 | },
37 |
38 | "DockerModelRunner": {
39 | "BaseUrl": "http://localhost:12434",
40 | "Model": "ai/smollm2"
41 | },
42 |
43 | "FoundryLocal": {
44 | "Alias": "phi-4-mini"
45 | },
46 |
47 | "HuggingFace": {
48 | "BaseUrl": "http://localhost:11434",
49 | "Model": "hf.co/Qwen/Qwen3-0.6B-GGUF"
50 | },
51 |
52 | "Ollama": {
53 | "BaseUrl": "http://localhost:11434",
54 | "Model": "llama3.2"
55 | },
56 |
57 | "Anthropic": {
58 | "ApiKey": "{{ANTHROPIC_API_KEY}}",
59 | "Model": "claude-sonnet-4-0"
60 | },
61 |
62 | "LG": {
63 | "BaseUrl": "http://localhost:11434",
64 | "Model": "hf.co/LGAI-EXAONE/EXAONE-4.0-1.2B-GGUF"
65 | },
66 |
67 | "OpenAI": {
68 | "ApiKey": "{{OPENAI_API_KEY}}",
69 | "Model": "gpt-4.1-mini"
70 | },
71 |
72 | "Upstage": {
73 | "BaseUrl": "https://api.upstage.ai/v1",
74 | "ApiKey": "{{UPSTAGE_API_KEY}}",
75 | "Model": "solar-mini"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/android-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/android-icon-144x144.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/android-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/android-icon-192x192.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/android-icon-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/android-icon-36x36.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/android-icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/android-icon-48x48.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/android-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/android-icon-72x72.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/android-icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/android-icon-96x96.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/app.css:
--------------------------------------------------------------------------------
1 | @import url('lib/tailwindcss/dist/preflight.css');
2 |
3 | html {
4 | min-height: 100vh;
5 | }
6 |
7 | html, .main-background-gradient {
8 | background: linear-gradient(to bottom, rgb(225 227 233), #f4f4f4 25rem);
9 | }
10 |
11 | body {
12 | display: flex;
13 | flex-direction: column;
14 | min-height: 100vh;
15 | }
16 |
17 | html::after {
18 | content: '';
19 | background-image: linear-gradient(to right, #3a4ed5, #3acfd5 15%, #d53abf 85%, red);
20 | width: 100%;
21 | height: 2px;
22 | position: fixed;
23 | top: 0;
24 | }
25 |
26 | h1 {
27 | font-size: 2.25rem;
28 | line-height: 2.5rem;
29 | font-weight: 600;
30 | }
31 |
32 | h1:focus {
33 | outline: none;
34 | }
35 |
36 | .valid.modified:not([type=checkbox]) {
37 | outline: 1px solid #26b050;
38 | }
39 |
40 | .invalid {
41 | outline: 1px solid #e50000;
42 | }
43 |
44 | .validation-message {
45 | color: #e50000;
46 | }
47 |
48 | .blazor-error-boundary {
49 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
50 | padding: 1rem 1rem 1rem 3.7rem;
51 | color: white;
52 | }
53 |
54 | .blazor-error-boundary::after {
55 | content: "An error has occurred."
56 | }
57 |
58 | .btn-default {
59 | display: flex;
60 | padding: 0.25rem 0.75rem;
61 | gap: 0.25rem;
62 | align-items: center;
63 | border-radius: 0.25rem;
64 | border: 1px solid #9CA3AF;
65 | font-size: 0.875rem;
66 | line-height: 1.25rem;
67 | font-weight: 600;
68 | background-color: #D1D5DB;
69 | }
70 |
71 | .btn-default:hover {
72 | background-color: #E5E7EB;
73 | }
74 |
75 | .btn-subtle {
76 | display: flex;
77 | padding: 0.25rem 0.75rem;
78 | gap: 0.25rem;
79 | align-items: center;
80 | border-radius: 0.25rem;
81 | border: 1px solid #D1D5DB;
82 | font-size: 0.875rem;
83 | line-height: 1.25rem;
84 | }
85 |
86 | .btn-subtle:hover {
87 | border-color: #93C5FD;
88 | background-color: #DBEAFE;
89 | }
90 |
91 | .page-width {
92 | max-width: 1024px;
93 | margin: auto;
94 | }
95 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-114x114.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-120x120.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-144x144.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-152x152.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-180x180.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-57x57.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-60x60.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-72x72.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-76x76.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-precomposed.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 | #ffffff
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/favicon-16x16.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/favicon-32x32.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/favicon-96x96.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/lib/dompurify/README.md:
--------------------------------------------------------------------------------
1 | dompurify version 3.2.4
2 | https://github.com/cure53/DOMPurify
3 | License: Apache 2.0 and Mozilla Public License 2.0
4 |
5 | To update, replace the files with an updated build from https://www.npmjs.com/package/dompurify
6 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/lib/marked/README.md:
--------------------------------------------------------------------------------
1 | marked version 15.0.6
2 | https://github.com/markedjs/marked
3 | License: MIT
4 |
5 | To update, replace the files with with an updated build from https://www.npmjs.com/package/marked
6 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/lib/tailwindcss/README.md:
--------------------------------------------------------------------------------
1 | tailwindcss version 4.0.3
2 | https://github.com/tailwindlabs/tailwindcss
3 | License: MIT
4 |
5 | This template uses only `preflight.css`, the CSS reset stylesheet from Tailwind. For simplicity, this template doesn't use a complete deployment of Tailwind. If you want to use the full Tailwind library, remove `preflight.css` and reference the full version of Tailwind.
6 |
7 | To update, replace the `preflight.css` file with the one in an updated build from https://www.npmjs.com/package/tailwindcss
8 |
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "OpenChat Playground",
3 | "icons": [
4 | {
5 | "src": "\/android-icon-36x36.png",
6 | "sizes": "36x36",
7 | "type": "image\/png",
8 | "density": "0.75"
9 | },
10 | {
11 | "src": "\/android-icon-48x48.png",
12 | "sizes": "48x48",
13 | "type": "image\/png",
14 | "density": "1.0"
15 | },
16 | {
17 | "src": "\/android-icon-72x72.png",
18 | "sizes": "72x72",
19 | "type": "image\/png",
20 | "density": "1.5"
21 | },
22 | {
23 | "src": "\/android-icon-96x96.png",
24 | "sizes": "96x96",
25 | "type": "image\/png",
26 | "density": "2.0"
27 | },
28 | {
29 | "src": "\/android-icon-144x144.png",
30 | "sizes": "144x144",
31 | "type": "image\/png",
32 | "density": "3.0"
33 | },
34 | {
35 | "src": "\/android-icon-192x192.png",
36 | "sizes": "192x192",
37 | "type": "image\/png",
38 | "density": "4.0"
39 | }
40 | ]
41 | }
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/ms-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/ms-icon-144x144.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/ms-icon-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/ms-icon-150x150.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/ms-icon-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/ms-icon-310x310.png
--------------------------------------------------------------------------------
/src/OpenChat.PlaygroundApp/wwwroot/ms-icon-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/ms-icon-70x70.png
--------------------------------------------------------------------------------
/test/OpenChat.ConsoleApp.Tests/OpenChat.ConsoleApp.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/test/OpenChat.ConsoleApp.Tests/Options/ArgumentOptionsTests.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | using Microsoft.Extensions.Configuration;
4 |
5 | using OpenChat.ConsoleApp.Options;
6 |
7 | namespace OpenChat.ConsoleApp.Tests.Options;
8 |
9 | public class ArgumentOptionsTests
10 | {
11 | private static IConfiguration BuildConfig(string? endpoint = null)
12 | {
13 | var values = new Dictionary
14 | {
15 | ["ApiApp:Endpoint"] = endpoint
16 | }!;
17 |
18 | return new ConfigurationBuilder()
19 | .AddInMemoryCollection(values!)
20 | .Build();
21 | }
22 |
23 | [Trait("Category", "UnitTest")]
24 | [Fact]
25 | public void Given_EndpointArgument_When_Parse_Then_Should_Set_Endpoint_And_Help_False()
26 | {
27 | // Arrange
28 | var config = BuildConfig(endpoint: "http://default");
29 | var expected = "http://localhost:1234";
30 | var args = new[] { "--endpoint", expected };
31 |
32 | // Act
33 | var result = ArgumentOptions.Parse(config, args);
34 |
35 | // Assert
36 | result.ShouldNotBeNull();
37 | result.ApiApp.Endpoint.ShouldBe(expected);
38 | result.Help.ShouldBeFalse();
39 | }
40 |
41 | [Trait("Category", "UnitTest")]
42 | [Theory]
43 | [InlineData("--help")]
44 | [InlineData("-h")]
45 | public void Given_HelpArguments_When_Parse_Then_Should_Set_Help_True(string arg)
46 | {
47 | // Arrange
48 | var config = BuildConfig();
49 | var args = new[] { arg };
50 |
51 | // Act
52 | var result = ArgumentOptions.Parse(config, args);
53 |
54 | // Assert
55 | result.Help.ShouldBeTrue();
56 | }
57 |
58 | [Trait("Category", "UnitTest")]
59 | [Fact]
60 | public void Given_UnknownArgument_When_Parse_Then_Should_Set_Help_True()
61 | {
62 | // Arrange
63 | var config = BuildConfig();
64 | var args = new[] { "--unknown" };
65 |
66 | // Act
67 | var result = ArgumentOptions.Parse(config, args);
68 |
69 | // Assert
70 | result.Help.ShouldBeTrue();
71 | }
72 |
73 | [Trait("Category", "UnitTest")]
74 | [Fact]
75 | public void Given_EndpointArgumentWithoutValue_When_Parse_Then_Should_NotThrow_And_Help_False()
76 | {
77 | // Arrange
78 | var config = BuildConfig();
79 | var args = new[] { "--endpoint" }; // Missing value intentionally.
80 |
81 | // Act
82 | var result = ArgumentOptions.Parse(config, args);
83 |
84 | // Assert
85 | result.Help.ShouldBeFalse();
86 | result.ApiApp.Endpoint.ShouldBeNull();
87 | }
88 |
89 | [Trait("Category", "UnitTest")]
90 | [Fact]
91 | public void Given_DisplayHelp_When_Called_Then_Should_Write_Expected_Lines()
92 | {
93 | // Arrange
94 | var output = new StringBuilder();
95 | using var writer = new StringWriter(output);
96 | var originalOut = Console.Out;
97 | Console.SetOut(writer);
98 |
99 | try
100 | {
101 | // Act
102 | ArgumentOptions.DisplayHelp();
103 | }
104 | finally
105 | {
106 | Console.SetOut(originalOut);
107 | }
108 |
109 | // Assert
110 | var text = output.ToString();
111 | text.ShouldContain("OpenChat Playground");
112 | text.ShouldContain("--endpoint");
113 | text.ShouldContain("--help");
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/Abstractions/LanguageModelConnectorTests.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Configurations;
3 | using OpenChat.PlaygroundApp.Connectors;
4 |
5 | namespace OpenChat.PlaygroundApp.Tests.Abstractions;
6 |
7 | public class LanguageModelConnectorTests
8 | {
9 | private static AppSettings BuildAppSettings(
10 | ConnectorType connectorType = ConnectorType.GitHubModels,
11 | string? endpoint = "https://models.github.ai/inference",
12 | string? token = "test-token",
13 | string? model = "openai/gpt-4o-mini")
14 | {
15 | return new AppSettings
16 | {
17 | ConnectorType = connectorType,
18 | GitHubModels = new GitHubModelsSettings
19 | {
20 | Endpoint = endpoint,
21 | Token = token,
22 | Model = model
23 | }
24 | };
25 | }
26 |
27 | [Trait("Category", "UnitTest")]
28 | [Fact]
29 | public async Task Given_GitHubModels_Settings_When_CreateChatClient_Invoked_Then_It_Should_Return_ChatClient()
30 | {
31 | // Arrange
32 | var settings = BuildAppSettings();
33 |
34 | // Act
35 | var client = await LanguageModelConnector.CreateChatClientAsync(settings);
36 |
37 | // Assert
38 | client.ShouldNotBeNull();
39 | }
40 |
41 | [Trait("Category", "UnitTest")]
42 | [Theory]
43 | [InlineData(ConnectorType.Unknown)]
44 | public async Task Given_Unsupported_ConnectorType_When_CreateChatClient_Invoked_Then_It_Should_Throw(ConnectorType connectorType)
45 | {
46 | // Arrange
47 | var settings = BuildAppSettings(connectorType: connectorType);
48 |
49 | // Act
50 | var ex = await Assert.ThrowsAsync(() => LanguageModelConnector.CreateChatClientAsync(settings));
51 |
52 | // Assert
53 | ex.Message.ShouldContain($"Connector type '{connectorType}'");
54 | }
55 |
56 | [Trait("Category", "UnitTest")]
57 | [Theory]
58 | // [InlineData(typeof(AmazonBedrockConnector))]
59 | // [InlineData(typeof(AzureAIFoundryConnector))]
60 | [InlineData(typeof(GitHubModelsConnector))]
61 | // [InlineData(typeof(GoogleVertexAIConnector))]
62 | // [InlineData(typeof(DockerModelRunnerConnector))]
63 | // [InlineData(typeof(FoundryLocalConnector))]
64 | [InlineData(typeof(HuggingFaceConnector))]
65 | // [InlineData(typeof(OllamaConnector))]
66 | // [InlineData(typeof(AnthropicConnector))]
67 | // [InlineData(typeof(LGConnector))]
68 | // [InlineData(typeof(NaverConnector))]
69 | [InlineData(typeof(OpenAIConnector))]
70 | // [InlineData(typeof(UpstageConnector))]
71 | public void Given_Concrete_Connectors_When_Checking_Inheritance_Then_Should_Inherit_From_LanguageModelConnector(Type derivedType)
72 | {
73 | // Arrange
74 | var baseType = typeof(LanguageModelConnector);
75 |
76 | // Act
77 | var result = baseType.IsAssignableFrom(derivedType);
78 |
79 | // Assert
80 | result.ShouldBeTrue();
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/Abstractions/LanguageModelSettingsTests.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Configurations;
3 |
4 | namespace OpenChat.PlaygroundApp.Tests.Abstractions;
5 |
6 | public class LanguageModelSettingsTests
7 | {
8 | [Trait("Category", "UnitTest")]
9 | [Theory]
10 | [InlineData(typeof(AmazonBedrockSettings))]
11 | [InlineData(typeof(AzureAIFoundrySettings))]
12 | [InlineData(typeof(GitHubModelsSettings))]
13 | [InlineData(typeof(GoogleVertexAISettings))]
14 | [InlineData(typeof(DockerModelRunnerSettings))]
15 | [InlineData(typeof(FoundryLocalSettings))]
16 | [InlineData(typeof(HuggingFaceSettings))]
17 | [InlineData(typeof(OllamaSettings))]
18 | [InlineData(typeof(AnthropicSettings))]
19 | [InlineData(typeof(LGSettings))]
20 | // [InlineData(typeof(NaverSettings))]
21 | [InlineData(typeof(OpenAISettings))]
22 | [InlineData(typeof(UpstageSettings))]
23 | public void Given_Concrete_Settings_When_Checking_Inheritance_Then_Should_Inherit_From_LanguageModelSettings(Type type)
24 | {
25 | // Act
26 | var isSubclass = type.IsSubclassOf(typeof(LanguageModelSettings));
27 |
28 | // Assert
29 | isSubclass.ShouldBeTrue();
30 | }
31 | }
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatHeaderUITests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Playwright;
2 | using Microsoft.Playwright.Xunit;
3 |
4 | using OpenChat.PlaygroundApp.Connectors;
5 |
6 | namespace OpenChat.PlaygroundApp.Tests.Components.Pages.Chat;
7 |
8 | public class ChatHeaderUITests : PageTest
9 | {
10 | public override async Task InitializeAsync()
11 | {
12 | await base.InitializeAsync();
13 | await Page.GotoAsync(TestConstants.LocalhostUrl);
14 | await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
15 | }
16 |
17 | [Trait("Category", "IntegrationTest")]
18 | [Theory]
19 | [InlineData("OpenChat Playground")]
20 | public async Task Given_Root_Page_When_Loaded_Then_Header_Should_Be_Visible(string expected)
21 | {
22 | // Act
23 | var title = await Page.Locator("span.app-title-text").InnerTextAsync();
24 |
25 | // Assert
26 | title.ShouldBe(expected);
27 | }
28 |
29 | [Trait("Category", "IntegrationTest")]
30 | [Fact]
31 | public async Task Given_Root_Page_When_Loaded_Then_Header_Should_Display_ConnectorType_And_Model()
32 | {
33 | // Act
34 | var connector = await Page.Locator("span.app-connector").InnerTextAsync();
35 | var model = await Page.Locator("span.app-model").InnerTextAsync();
36 |
37 | // Assert
38 | connector.ShouldNotBeNullOrEmpty();
39 | Enum.IsDefined(typeof(ConnectorType), connector).ShouldBeTrue();
40 | model.ShouldNotBeNullOrEmpty();
41 | }
42 |
43 | [Trait("Category", "IntegrationTest")]
44 | [Fact]
45 | public async Task Given_Root_Page_When_Loaded_Then_NewChat_Button_Should_Be_Visible()
46 | {
47 | // Arrange
48 | var newChatButton = Page.GetByRole(AriaRole.Button, new() { Name = "New chat" });
49 |
50 | // Assert
51 | var isVisible = await newChatButton.IsVisibleAsync();
52 | isVisible.ShouldBeTrue();
53 | }
54 |
55 | [Trait("Category", "IntegrationTest")]
56 | [Fact]
57 | public async Task Given_Header_When_Loaded_Then_NewChat_Icon_Should_Be_Visible()
58 | {
59 | // Arrange
60 | var icon = Page.Locator("button svg.new-chat-icon");
61 |
62 | // Assert
63 | var isVisible = await icon.IsVisibleAsync();
64 | isVisible.ShouldBeTrue();
65 | }
66 |
67 | [Trait("Category", "IntegrationTest")]
68 | [Trait("Category", "LLMRequired")]
69 | [Theory]
70 | [InlineData("1+1의 결과는 무엇인가요?")]
71 | [InlineData("what is the result of 1 + 1?")]
72 | public async Task Given_UserAndAssistantMessages_When_NewChat_Clicked_Then_Conversation_Should_Reset(string userMessage)
73 | {
74 | // Arrange
75 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" });
76 | var sendButton = Page.GetByRole(AriaRole.Button, new() { Name = "User Message Send Button" });
77 | var newChatButton = Page.GetByRole(AriaRole.Button, new() { Name = "New chat" });
78 |
79 | var loadingSpinner = Page.Locator(".lds-ellipsis");
80 | var userMessages = Page.Locator(".user-message");
81 | var assistantMessages = Page.Locator(".assistant-message-header");
82 | var noMessagesPlaceholder = Page.Locator(".no-messages");
83 |
84 | // Act
85 | await textArea.FillAsync(userMessage);
86 | await sendButton.ClickAsync();
87 | await newChatButton.ClickAsync();
88 | await noMessagesPlaceholder.WaitForAsync();
89 |
90 | // Assert
91 | var userMessageCount = await userMessages.CountAsync();
92 | userMessageCount.ShouldBe(0);
93 |
94 | var assistantMessageCount = await assistantMessages.CountAsync();
95 | assistantMessageCount.ShouldBe(0);
96 |
97 | var placeholderVisible = await noMessagesPlaceholder.IsVisibleAsync();
98 | placeholderVisible.ShouldBeTrue();
99 |
100 | var spinnerVisible = await loadingSpinner.IsVisibleAsync();
101 | spinnerVisible.ShouldBeFalse();
102 | }
103 |
104 | public override async Task DisposeAsync()
105 | {
106 | await Page.CloseAsync();
107 | await base.DisposeAsync();
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatInputImeE2ETests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Playwright;
2 | using Microsoft.Playwright.Xunit;
3 |
4 | namespace OpenChat.PlaygroundApp.Tests.Components.Pages.Chat;
5 |
6 | public class ChatInputImeE2ETests : PageTest
7 | {
8 | public override async Task InitializeAsync()
9 | {
10 | await base.InitializeAsync();
11 | await Page.GotoAsync(TestConstants.LocalhostUrl);
12 | await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
13 | }
14 |
15 | [Trait("Category", "IntegrationTest")]
16 | [Theory]
17 | [InlineData("안녕하세요")]
18 | [InlineData("테스트")]
19 | public async Task Given_Korean_IME_Composition_When_Enter_During_Composition_Then_It_Should_Not_Submit(string testMessage)
20 | {
21 | // Arrange
22 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" });
23 | await textArea.FocusAsync();
24 | await textArea.FillAsync(testMessage);
25 | var userCountBefore = await Page.Locator(".user-message").CountAsync();
26 |
27 | // Act: Enter during composition should NOT submit
28 | await Page.DispatchEventAsync("textarea", "compositionstart", new { });
29 | await Page.DispatchEventAsync("textarea", "keydown", new { bubbles = true, cancelable = true, key = "Enter", isComposing = true });
30 |
31 | // Assert: no user message added
32 | var userCountAfterComposeEnter = await Page.Locator(".user-message").CountAsync();
33 | userCountAfterComposeEnter.ShouldBe(userCountBefore);
34 | }
35 |
36 | [Trait("Category", "IntegrationTest")]
37 | [Trait("Category", "LLMRequired")]
38 | [Theory]
39 | [InlineData("안녕하세요", "안")]
40 | [InlineData("테스트", "테")]
41 | public async Task Given_Korean_IME_Composition_Ended_When_Enter_Pressed_Then_It_Should_Submit_Once(string testMessage, string compositionData)
42 | {
43 | // Arrange
44 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" });
45 | await textArea.FocusAsync();
46 | await textArea.FillAsync(testMessage);
47 | var userCountBefore = await Page.Locator(".user-message").CountAsync();
48 |
49 | // Act: Composition ends, then Enter should submit once
50 | await Page.DispatchEventAsync("textarea", "compositionstart", new { });
51 | await Page.DispatchEventAsync("textarea", "compositionend", new { data = compositionData });
52 | var assistantCountBefore = await Page.Locator(".assistant-message-header").CountAsync();
53 | await Page.DispatchEventAsync("textarea", "keydown", new { bubbles = true, cancelable = true, key = "Enter" });
54 |
55 | // Assert: assistant response begins and user message added once
56 | await Page.WaitForFunctionAsync(
57 | "args => document.querySelectorAll(args.selector).length >= args.expected",
58 | new { selector = ".assistant-message-header", expected = assistantCountBefore + 1 }
59 | );
60 | var userCountAfterSubmit = await Page.Locator(".user-message").CountAsync();
61 | userCountAfterSubmit.ShouldBe(userCountBefore + 1);
62 | }
63 |
64 | [Trait("Category", "IntegrationTest")]
65 | [Trait("Category", "LLMRequired")]
66 | [Theory]
67 | [InlineData("테스트 메시지")]
68 | [InlineData("안녕하세요")]
69 | public async Task Given_Message_Sent_When_Enter_Pressed_Immediately_Then_It_Should_Not_Send_Twice(string testMessage)
70 | {
71 | // Arrange
72 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" });
73 | await textArea.FocusAsync();
74 | await textArea.FillAsync(testMessage);
75 | var userCountBefore = await Page.Locator(".user-message").CountAsync();
76 |
77 | // Act: Send via Enter
78 | var assistantCountBefore = await Page.Locator(".assistant-message-header").CountAsync();
79 | await textArea.PressAsync("Enter");
80 |
81 | // Assert: assistant response begins and one user message
82 | await Page.WaitForFunctionAsync(
83 | "args => document.querySelectorAll(args.selector).length >= args.expected",
84 | new { selector = ".assistant-message-header", expected = assistantCountBefore + 1 }
85 | );
86 | var userCountAfterFirst = await Page.Locator(".user-message").CountAsync();
87 | userCountAfterFirst.ShouldBe(userCountBefore + 1);
88 |
89 | // Act: Press Enter again immediately without typing
90 | await textArea.PressAsync("Enter");
91 |
92 | // Assert: no additional user message
93 | var userCountAfterSecond = await Page.Locator(".user-message").CountAsync();
94 | userCountAfterSecond.ShouldBe(userCountBefore + 1);
95 | }
96 |
97 | [Trait("Category", "IntegrationTest")]
98 | [Theory]
99 | [InlineData("첫 줄")]
100 | [InlineData("테스트")]
101 | public async Task Given_Text_Input_When_Shift_Enter_Pressed_Then_It_Should_Insert_Newline_Not_Submit(string initialText)
102 | {
103 | // Arrange
104 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" });
105 | await textArea.FocusAsync();
106 | await textArea.FillAsync(initialText);
107 | var userCountBefore = await Page.Locator(".user-message").CountAsync();
108 |
109 | // Act: Shift+Enter should insert newline (not submit)
110 | await textArea.PressAsync("Shift+Enter");
111 |
112 | // Assert: value contains newline and no submission
113 | var value = await textArea.InputValueAsync();
114 | value.ShouldContain("\n");
115 | var userCountAfter = await Page.Locator(".user-message").CountAsync();
116 | userCountAfter.ShouldBe(userCountBefore);
117 | }
118 |
119 | public override async Task DisposeAsync()
120 | {
121 | await Page.CloseAsync();
122 | await base.DisposeAsync();
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatMessageItemUITests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Playwright;
2 | using Microsoft.Playwright.Xunit;
3 |
4 | namespace OpenChat.PlaygroundApp.Tests.Components.Pages.Chat;
5 |
6 | public class ChatMessageItemUITests : PageTest
7 | {
8 | public override async Task InitializeAsync()
9 | {
10 | await base.InitializeAsync();
11 | await Page.GotoAsync(TestConstants.LocalhostUrl);
12 | await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
13 | }
14 |
15 | [Trait("Category", "IntegrationTest")]
16 | [Theory]
17 | [InlineData("Input usermessage")]
18 | public async Task Given_UserMessage_When_Sent_Then_UserMessage_Count_Should_Increment(string userMessage)
19 | {
20 | // Arrange
21 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" });
22 | var userMessages = Page.Locator(".user-message");
23 | var initialCount = await userMessages.CountAsync();
24 |
25 | // Act
26 | await textArea.FillAsync(userMessage);
27 | await textArea.PressAsync("Enter");
28 | var newUserMessage = userMessages.Nth(initialCount);
29 | await newUserMessage.WaitForAsync(new() { State = WaitForSelectorState.Attached });
30 |
31 | // Assert
32 | var finalCount = await userMessages.CountAsync();
33 | finalCount.ShouldBe(initialCount + 1);
34 | }
35 |
36 | [Trait("Category", "IntegrationTest")]
37 | [Theory]
38 | [InlineData("Input usermessage")]
39 | public async Task Given_UserMessage_When_Sent_Then_Rendered_Text_Should_Match_UserMessage(string userMessage)
40 | {
41 | // Arrange
42 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" });
43 | var userMessages = Page.Locator(".user-message");
44 | var initialCount = await userMessages.CountAsync();
45 |
46 | // Act
47 | await textArea.FillAsync(userMessage);
48 | await textArea.PressAsync("Enter");
49 | var newUserMessage = userMessages.Nth(initialCount);
50 | await newUserMessage.WaitForAsync(new() { State = WaitForSelectorState.Attached });
51 |
52 | // Assert
53 | var latestUserMessage = userMessages.Nth(initialCount);
54 | var renderedText = await latestUserMessage.InnerTextAsync();
55 | renderedText.ShouldBe(userMessage);
56 | }
57 |
58 | [Trait("Category", "IntegrationTest")]
59 | [Trait("Category", "LLMRequired")]
60 | [Theory]
61 | [InlineData("Input usermessage")]
62 | public async Task Given_AssistantResponse_When_Streamed_Then_Latest_Text_Should_NotBeEmpty(string userMessage)
63 | {
64 | // Arrange
65 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" });
66 | var assistantTexts = Page.Locator(".assistant-message-text");
67 | var initialTextCount = await assistantTexts.CountAsync();
68 |
69 | // Act
70 | await textArea.FillAsync(userMessage);
71 | await textArea.PressAsync("Enter");
72 | var newAssistantText = assistantTexts.Nth(initialTextCount);
73 | await newAssistantText.WaitForAsync(new() { State = WaitForSelectorState.Attached });
74 |
75 | // Assert
76 | var finalContent = await newAssistantText.InnerTextAsync();
77 | finalContent.ShouldNotBeNullOrWhiteSpace();
78 | }
79 |
80 | [Trait("Category", "IntegrationTest")]
81 | [Trait("Category", "LLMRequired")]
82 | [Theory]
83 | [InlineData("Input usermessage")]
84 | public async Task Given_AssistantResponse_When_Message_Arrives_Then_Assistant_Icon_Should_Be_Visible(string userMessage)
85 | {
86 | // Arrange
87 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" });
88 | var assistantHeaders = Page.Locator(".assistant-message-header");
89 | var assistantIcons = Page.Locator(".assistant-message-icon svg");
90 | var initialHeaderCount = await assistantHeaders.CountAsync();
91 |
92 | // Act
93 | await textArea.FillAsync(userMessage);
94 | await textArea.PressAsync("Enter");
95 | var newAssistantHeader = assistantHeaders.Nth(initialHeaderCount);
96 | await newAssistantHeader.WaitForAsync(new() { State = WaitForSelectorState.Attached });
97 |
98 | // Assert
99 | var finalIconCount = await assistantIcons.CountAsync();
100 | var iconIndex = finalIconCount - 1;
101 | var iconVisible = await assistantIcons.Nth(iconIndex).IsVisibleAsync();
102 | iconVisible.ShouldBeTrue();
103 | }
104 |
105 | [Trait("Category", "IntegrationTest")]
106 | [Trait("Category", "LLMRequired")]
107 | [Theory]
108 | [InlineData("Input usermessage")]
109 | public async Task Given_Response_InProgress_When_Stream_Completes_Then_Spinner_Should_Toggle(string userMessage)
110 | {
111 | // Arrange
112 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" });
113 | var spinner = Page.Locator(".lds-ellipsis");
114 |
115 | // Act
116 | await textArea.FillAsync(userMessage);
117 | await textArea.PressAsync("Enter");
118 | await spinner.WaitForAsync(new() { State = WaitForSelectorState.Visible });
119 | var visibleWhileStreaming = await spinner.IsVisibleAsync();
120 | await spinner.WaitForAsync(new() { State = WaitForSelectorState.Hidden });
121 | var visibleAfterComplete = await spinner.IsVisibleAsync();
122 |
123 | // Assert
124 | visibleWhileStreaming.ShouldBeTrue();
125 | visibleAfterComplete.ShouldBeFalse();
126 | }
127 |
128 | public override async Task DisposeAsync()
129 | {
130 | await Page.CloseAsync();
131 | await base.DisposeAsync();
132 | }
133 | }
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatStreamingUITest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Playwright;
2 | using Microsoft.Playwright.Xunit;
3 |
4 | namespace OpenChat.PlaygroundApp.Tests.Components.Pages.Chat;
5 |
6 | public class ChatStreamingUITest : PageTest
7 | {
8 | public override async Task InitializeAsync()
9 | {
10 | await base.InitializeAsync();
11 | await Page.GotoAsync(TestConstants.LocalhostUrl);
12 | await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
13 | }
14 |
15 | [Trait("Category", "IntegrationTest")]
16 | [Trait("Category", "LLMRequired")]
17 | [Theory]
18 | [InlineData("하늘은 왜 푸른 색인가요? 다섯 개의 단락으로 자세히 설명해주세요.")]
19 | [InlineData("Why is the sky blue? Please explain in five paragraphs.")]
20 | public async Task Given_UserMessage_When_SendButton_Clicked_Then_Response_Should_Stream_Progressively(string userMessage)
21 | {
22 | // Arrange
23 | const int timeoutMs = 5000;
24 |
25 | const string messageSelector = ".assistant-message-text";
26 |
27 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" });
28 | var sendButton = Page.GetByRole(AriaRole.Button, new() { Name = "User Message Send Button" });
29 | var message = Page.Locator(messageSelector);
30 |
31 | // Act
32 | await textArea.FillAsync(userMessage);
33 | await sendButton.ClickAsync();
34 |
35 | // Assert
36 | await Expect(message).ToBeVisibleAsync(new() { Timeout = timeoutMs });
37 | await Expect(message).Not.ToHaveTextAsync(string.Empty, new() { Timeout = timeoutMs });
38 |
39 | var initialContent = await message.InnerTextAsync();
40 |
41 | await Expect(message).Not.ToHaveTextAsync(initialContent, new() { Timeout = timeoutMs });
42 |
43 | var finalContent = await message.InnerTextAsync();
44 |
45 | finalContent.ShouldNotBe(initialContent);
46 | finalContent.ShouldStartWith(initialContent);
47 | finalContent.Length.ShouldBeGreaterThan(initialContent.Length);
48 | }
49 |
50 | [Trait("Category", "IntegrationTest")]
51 | [Trait("Category", "LLMRequired")]
52 | [Theory]
53 | [InlineData("하늘은 왜 푸른 색인가요?")]
54 | [InlineData("Why is the sky blue?")]
55 | public async Task Given_UserMessage_When_SendButton_Clicked_Then_LoadingSpinner_Should_Be_Visible_Before_Text_Arrives(string userMessage)
56 | {
57 | // Arrange
58 | const string spinnerSelector = ".lds-ellipsis";
59 |
60 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" });
61 | var sendButton = Page.GetByRole(AriaRole.Button, new() { Name = "User Message Send Button" });
62 | var loadingSpinner = Page.Locator(spinnerSelector);
63 |
64 | // Act
65 | await textArea.FillAsync(userMessage);
66 | await sendButton.ClickAsync();
67 |
68 | // Assert
69 | await Page.WaitForSelectorAsync(spinnerSelector, new() { State = WaitForSelectorState.Visible });
70 |
71 | var spinnerVisible = await loadingSpinner.IsVisibleAsync();
72 | spinnerVisible.ShouldBeTrue();
73 | }
74 |
75 | [Trait("Category", "IntegrationTest")]
76 | [Trait("Category", "LLMRequired")]
77 | [Theory]
78 | [InlineData("하늘은 왜 푸른 색인가요?")]
79 | [InlineData("Why is the sky blue?")]
80 | public async Task Given_UserMessage_When_Response_Text_Arrives_Then_LoadingSpinner_Should_Disappear(string userMessage)
81 | {
82 | // Arrange
83 | const string spinnerSelector = ".lds-ellipsis";
84 | const string messageSelector = ".assistant-message-text";
85 |
86 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" });
87 | var sendButton = Page.GetByRole(AriaRole.Button, new() { Name = "User Message Send Button" });
88 | var loadingSpinner = Page.Locator(spinnerSelector);
89 | var message = Page.Locator(messageSelector);
90 |
91 | // Act
92 | await textArea.FillAsync(userMessage);
93 | await sendButton.ClickAsync();
94 |
95 | // Assert
96 | var messageContent = await message.InnerTextAsync();
97 | messageContent.ShouldNotBeEmpty();
98 |
99 | var spinnerVisible = await loadingSpinner.IsVisibleAsync();
100 | spinnerVisible.ShouldBeFalse();
101 | }
102 |
103 | public override async Task DisposeAsync()
104 | {
105 | await Page.CloseAsync();
106 | await base.DisposeAsync();
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatUITests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Playwright;
2 | using Microsoft.Playwright.Xunit;
3 |
4 | namespace OpenChat.PlaygroundApp.Tests.Components.Pages.Chat;
5 |
6 | public class ChatUITests : PageTest
7 | {
8 | public override async Task InitializeAsync()
9 | {
10 | await base.InitializeAsync();
11 | await Page.GotoAsync(TestConstants.LocalhostUrl);
12 | await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
13 | }
14 |
15 | [Trait("Category", "IntegrationTest")]
16 | [Fact]
17 | public async Task Given_Root_Page_When_Loaded_Then_NoMessagesContent_Should_Be_Visible()
18 | {
19 | // Arrange
20 | var noMessages = Page.Locator(".no-messages");
21 |
22 | // Act
23 | var isVisible = await noMessages.IsVisibleAsync();
24 |
25 | // Assert
26 | isVisible.ShouldBeTrue();
27 | }
28 |
29 | [Trait("Category", "IntegrationTest")]
30 | [Fact]
31 | public async Task Given_Root_Page_When_Loaded_Then_NoMessagesContent_Text_Should_Match()
32 | {
33 | // Arrange
34 | var noMessages = Page.Locator(".no-messages");
35 |
36 | // Act
37 | var text = await noMessages.InnerTextAsync();
38 |
39 | // Assert
40 | text.ShouldBe("To get started, try asking about anything.");
41 | }
42 |
43 | [Trait("Category", "IntegrationTest")]
44 | [Fact]
45 | public async Task Given_Root_Page_When_Loaded_Then_PageTitle_Should_Be_Visible()
46 | {
47 | // Act
48 | var headTitle = Page.Locator("title");
49 | var count = await headTitle.CountAsync();
50 |
51 | // Assert
52 | count.ShouldBeGreaterThan(0);
53 | }
54 |
55 |
56 | [Trait("Category", "IntegrationTest")]
57 | [Fact]
58 | public async Task Given_Root_Page_When_Loaded_Then_PageTitle_Text_Should_Match()
59 | {
60 | // Act
61 | var title = await Page.TitleAsync();
62 |
63 | // Assert
64 | title.ShouldBe("OpenChat Playground");
65 | }
66 |
67 | [Trait("Category", "IntegrationTest")]
68 | [Fact]
69 | public async Task Given_NewChat_Clicked_Then_Input_Textarea_Should_Be_Focused()
70 | {
71 | // Arrange
72 | var newChatButton = Page.GetByRole(AriaRole.Button, new() { Name = "New chat" });
73 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" });
74 |
75 | // Act
76 | await newChatButton.ClickAsync();
77 |
78 | // Assert
79 | await Expect(textArea).ToBeFocusedAsync();
80 | }
81 |
82 | [Trait("Category", "IntegrationTest")]
83 | [Fact]
84 | public async Task Given_NewChat_Clicked_Then_Conversation_Should_Reset_To_No_UserMessages()
85 | {
86 | // Arrange
87 | var newChatButton = Page.GetByRole(AriaRole.Button, new() { Name = "New chat" });
88 | var userMessageLocator = Page.Locator(".user-message");
89 |
90 | // Act
91 | await newChatButton.ClickAsync();
92 |
93 | // Assert
94 | await Expect(userMessageLocator).ToHaveCountAsync(0);
95 | }
96 |
97 | public override async Task DisposeAsync()
98 | {
99 | await Page.CloseAsync();
100 | await base.DisposeAsync();
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/Connectors/HuggingFaceConnectorTests.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Configurations;
3 | using OpenChat.PlaygroundApp.Connectors;
4 |
5 | namespace OpenChat.PlaygroundApp.Tests.Connectors;
6 |
7 | public class HuggingFaceConnectorTests
8 | {
9 | private const string BaseUrl = "https://test.huggingface.co/api";
10 | private const string Model = "hf.co/test-org/model-gguf";
11 |
12 | private static AppSettings BuildAppSettings(string? baseUrl = BaseUrl, string? model = Model)
13 | {
14 | return new AppSettings
15 | {
16 | ConnectorType = ConnectorType.HuggingFace,
17 | HuggingFace = new HuggingFaceSettings
18 | {
19 | BaseUrl = baseUrl,
20 | Model = model
21 | }
22 | };
23 | }
24 |
25 | [Trait("Category", "UnitTest")]
26 | [Theory]
27 | [InlineData(typeof(LanguageModelConnector), typeof(HuggingFaceConnector), true)]
28 | [InlineData(typeof(HuggingFaceConnector), typeof(LanguageModelConnector), false)]
29 | public void Given_BaseType_Then_It_Should_Be_AssignableFrom_DerivedType(Type baseType, Type derivedType, bool expected)
30 | {
31 | // Act
32 | var result = baseType.IsAssignableFrom(derivedType);
33 |
34 | // Assert
35 | result.ShouldBe(expected);
36 | }
37 |
38 | [Trait("Category", "UnitTest")]
39 | [Fact]
40 | public void Given_Settings_Is_Null_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw()
41 | {
42 | // Arrange
43 | var settings = new AppSettings { ConnectorType = ConnectorType.HuggingFace, HuggingFace = null };
44 | var connector = new HuggingFaceConnector(settings);
45 |
46 | // Act
47 | var ex = Assert.Throws(() => connector.EnsureLanguageModelSettingsValid());
48 |
49 | // Assert
50 | ex.Message.ShouldContain("HuggingFace");
51 | }
52 |
53 | [Trait("Category", "UnitTest")]
54 | [Theory]
55 | [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of an object")]
56 | [InlineData("", typeof(InvalidOperationException), "HuggingFace:BaseUrl")]
57 | [InlineData(" ", typeof(InvalidOperationException), "HuggingFace:BaseUrl")]
58 | public void Given_Invalid_BaseUrl_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? baseUrl, Type expectedType, string expectedMessage)
59 | {
60 | // Arrange
61 | var settings = BuildAppSettings(baseUrl: baseUrl);
62 | var connector = new HuggingFaceConnector(settings);
63 |
64 | // Act
65 | var ex = Assert.Throws(expectedType, () => connector.EnsureLanguageModelSettingsValid());
66 |
67 | // Assert
68 | ex.Message.ShouldContain(expectedMessage);
69 | }
70 |
71 | [Trait("Category", "UnitTest")]
72 | [Theory]
73 | [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of an object")]
74 | [InlineData("", typeof(InvalidOperationException), "HuggingFace:Model")]
75 | [InlineData(" ", typeof(InvalidOperationException), "HuggingFace:Model")]
76 | [InlineData("hf.co/org/model", typeof(InvalidOperationException), "HuggingFace:Model format")]
77 | [InlineData("org/model-gguf", typeof(InvalidOperationException), "HuggingFace:Model format")]
78 | [InlineData("hf.co//model-gguf", typeof(InvalidOperationException), "HuggingFace:Model format")]
79 | public void Given_Invalid_Model_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? model, Type expectedType, string expectedMessage)
80 | {
81 | // Arrange
82 | var settings = BuildAppSettings(model: model);
83 | var connector = new HuggingFaceConnector(settings);
84 |
85 | // Act
86 | var ex = Assert.Throws(expectedType, () => connector.EnsureLanguageModelSettingsValid());
87 |
88 | // Assert
89 | ex.Message.ShouldContain(expectedMessage);
90 | }
91 |
92 | [Trait("Category", "UnitTest")]
93 | [Fact]
94 | public void Given_Valid_Settings_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Return_True()
95 | {
96 | // Arrange
97 | var settings = BuildAppSettings();
98 | var connector = new HuggingFaceConnector(settings);
99 |
100 | // Act
101 | var result = connector.EnsureLanguageModelSettingsValid();
102 |
103 | // Assert
104 | result.ShouldBeTrue();
105 | }
106 |
107 | [Trait("Category", "IntegrationTest")]
108 | [Trait("Category", "LLMRequired")]
109 | [Fact]
110 | public async Task Given_Valid_Settings_When_GetChatClient_Invoked_Then_It_Should_Return_ChatClient()
111 | {
112 | // Arrange
113 | var settings = BuildAppSettings();
114 | var connector = new HuggingFaceConnector(settings);
115 |
116 | // Act
117 | var client = await connector.GetChatClientAsync();
118 |
119 | // Assert
120 | client.ShouldNotBeNull();
121 | }
122 |
123 | [Trait("Category", "UnitTest")]
124 | [Theory]
125 | [InlineData(null, typeof(ArgumentNullException), "null")]
126 | [InlineData("", typeof(UriFormatException), "empty")]
127 | public async Task Given_Missing_BaseUrl_When_GetChatClient_Invoked_Then_It_Should_Throw(string? baseUrl, Type expected, string message)
128 | {
129 | // Arrange
130 | var settings = BuildAppSettings(baseUrl: baseUrl);
131 | var connector = new HuggingFaceConnector(settings);
132 |
133 | // Act
134 | var ex = await Assert.ThrowsAsync(expected, connector.GetChatClientAsync);
135 |
136 | // Assert
137 | ex.Message.ShouldContain(message);
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/Endpoints/ChatResponseEndpointTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Routing;
3 | using Microsoft.Extensions.Logging;
4 |
5 | using OpenChat.PlaygroundApp.Endpoints;
6 | using OpenChat.PlaygroundApp.Services;
7 |
8 | namespace OpenChat.PlaygroundApp.Tests.Endpoints;
9 |
10 | public class ChatResponseEndpointTests
11 | {
12 | [Trait("Category", "UnitTest")]
13 | [Fact]
14 | public void Given_Null_IChatService_When_ChatResponseEndpoint_Instantiated_Then_It_Should_Throw()
15 | {
16 | // Arrange
17 | var logger = Substitute.For>();
18 |
19 | // Act
20 | Action action = () => new ChatResponseEndpoint(default(IChatService)!, logger);
21 |
22 | // Assert
23 | action.ShouldThrow();
24 | }
25 |
26 | [Trait("Category", "UnitTest")]
27 | [Fact]
28 | public void Given_Null_Logger_When_ChatResponseEndpoint_Instantiated_Then_It_Should_Throw()
29 | {
30 | // Arrange
31 | var service = Substitute.For();
32 |
33 | // Act
34 | Action action = () => new ChatResponseEndpoint(service, default(ILogger)!);
35 |
36 | // Assert
37 | action.ShouldThrow();
38 | }
39 |
40 | [Trait("Category", "UnitTest")]
41 | [Fact]
42 | public void Given_Both_Dependencies_When_ChatResponseEndpoint_Instantiated_Then_It_Should_Create()
43 | {
44 | // Arrange
45 | var service = Substitute.For();
46 | var logger = Substitute.For>();
47 |
48 | // Act
49 | var result = new ChatResponseEndpoint(service, logger);
50 |
51 | // Assert
52 | result.ShouldNotBeNull();
53 | }
54 |
55 | [Trait("Category", "UnitTest")]
56 | [Theory]
57 | [InlineData("/chat/responses")]
58 | public void Given_Endpoint_When_MapEndpoint_Invoked_Then_It_Should_Contain(string pattern)
59 | {
60 | // Arrange
61 | var args = Array.Empty();
62 | var app = WebApplication.CreateBuilder(args).Build();
63 | var chatService = Substitute.For();
64 | var logger = Substitute.For>();
65 | var endpoint = new ChatResponseEndpoint(chatService, logger);
66 |
67 | // Act
68 | endpoint.MapEndpoint(app);
69 | var result = app.GetType()
70 | .GetProperty("DataSources", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!
71 | .GetValue(app) as ICollection;
72 |
73 | // Assert
74 | result.ShouldNotBeNull()
75 | .First()
76 | .Endpoints.OfType()
77 | .Any(e => e.RoutePattern.RawText?.Equals(pattern, StringComparison.OrdinalIgnoreCase) ?? false)
78 | .ShouldBeTrue();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/Endpoints/TestEndpoint.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Routing;
2 |
3 | using OpenChat.PlaygroundApp.Endpoints;
4 |
5 | namespace OpenChat.PlaygroundApp.Tests.Endpoints;
6 |
7 | public partial class EndpointExtensionsTests
8 | {
9 | private class TestEndpoint : IEndpoint
10 | {
11 | public void MapEndpoint(IEndpointRouteBuilder app)
12 | {
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/OpenChat.PlaygroundApp.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | PreserveNewest
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/Options/DockerModelRunnerArgumentOptionsTests.cs:
--------------------------------------------------------------------------------
1 | using OpenChat.PlaygroundApp.Abstractions;
2 | using OpenChat.PlaygroundApp.Options;
3 |
4 | namespace OpenChat.PlaygroundApp.Tests.Options;
5 |
6 | public class DockerModelRunnerArgumentOptionsTests
7 | {
8 | [Trait("Category", "UnitTest")]
9 | [Theory]
10 | [InlineData(typeof(ArgumentOptions), typeof(DockerModelRunnerArgumentOptions), true)]
11 | [InlineData(typeof(DockerModelRunnerArgumentOptions), typeof(ArgumentOptions), false)]
12 | public void Given_BaseType_Then_It_Should_Be_AssignableFrom_DerivedType(Type baseType, Type derivedType, bool expected)
13 | {
14 | // Act
15 | var result = baseType.IsAssignableFrom(derivedType);
16 |
17 | // Assert
18 | result.ShouldBe(expected);
19 | }
20 | }
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/Services/ChatServiceTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.AI;
2 | using Microsoft.Extensions.Logging;
3 |
4 | using OpenChat.PlaygroundApp.Services;
5 |
6 | namespace OpenChat.PlaygroundApp.Tests.Services;
7 |
8 | public class ChatServiceTests
9 | {
10 | [Trait("Category", "UnitTest")]
11 | [Fact]
12 | public void Given_Null_IChatClient_When_ChatService_Instantiated_Then_It_Should_Throw()
13 | {
14 | // Arrange
15 | var logger = Substitute.For>();
16 |
17 | // Act
18 | Action action = () => new ChatService(default(IChatClient)!, logger);
19 |
20 | // Assert
21 | action.ShouldThrow();
22 | }
23 |
24 | [Trait("Category", "UnitTest")]
25 | [Fact]
26 | public void Given_Null_Logger_When_ChatService_Instantiated_Then_It_Should_Throw()
27 | {
28 | // Arrange
29 | var client = Substitute.For();
30 |
31 | // Act
32 | Action action = () => new ChatService(client, default(ILogger)!);
33 |
34 | // Assert
35 | action.ShouldThrow();
36 | }
37 |
38 | [Trait("Category", "UnitTest")]
39 | [Fact]
40 | public void Given_Both_Dependencies_When_ChatService_Instantiated_Then_It_Should_Create()
41 | {
42 | // Arrange
43 | var client = Substitute.For();
44 | var logger = Substitute.For>();
45 |
46 | // Act
47 | var result = new ChatService(client, logger);
48 |
49 | // Assert
50 | result.ShouldNotBeNull();
51 | }
52 |
53 | [Trait("Category", "UnitTest")]
54 | [Fact]
55 | public void Given_Less_Than_Two_Messages_When_GetStreamingResponseAsync_Invoked_Then_It_Should_Throw()
56 | {
57 | // Arrange
58 | var chatClient = Substitute.For();
59 | var logger = Substitute.For>();
60 | var chatService = new ChatService(chatClient, logger);
61 |
62 | var messages = new List
63 | {
64 | new(ChatRole.User, "Hello")
65 | };
66 |
67 | // Act
68 | Action action = () => chatService.GetStreamingResponseAsync(messages);
69 |
70 | // Assert
71 | action.ShouldThrow()
72 | .Message.ShouldContain("At least two messages are required");
73 | }
74 |
75 | [Trait("Category", "UnitTest")]
76 | [Fact]
77 | public void Given_First_Message_Is_Not_System_When_GetStreamingResponseAsync_Invoked_Then_It_Should_Throw()
78 | {
79 | // Arrange
80 | var chatClient = Substitute.For();
81 | var logger = Substitute.For>();
82 | var chatService = new ChatService(chatClient, logger);
83 |
84 | var messages = new List
85 | {
86 | new(ChatRole.User, "Hello"),
87 | new(ChatRole.User, "How are you?")
88 | };
89 |
90 | // Act
91 | Action action = () => chatService.GetStreamingResponseAsync(messages);
92 |
93 | // Assert
94 | action.ShouldThrow()
95 | .Message.ShouldContain("The first message must be a system message");
96 | }
97 |
98 | [Trait("Category", "UnitTest")]
99 | [Fact]
100 | public void Given_Second_Message_Is_Not_User_When_GetStreamingResponseAsync_Invoked_Then_It_Should_Throw()
101 | {
102 | // Arrange
103 | var chatClient = Substitute.For();
104 | var logger = Substitute.For>();
105 | var chatService = new ChatService(chatClient, logger);
106 |
107 | var messages = new List
108 | {
109 | new(ChatRole.System, "You are a helpful assistant."),
110 | new(ChatRole.Assistant, "Why is the sky blue?")
111 | };
112 |
113 | // Act
114 | Action action = () => chatService.GetStreamingResponseAsync(messages);
115 |
116 | // Assert
117 | action.ShouldThrow()
118 | .Message.ShouldContain("The second message must be a user message");
119 | }
120 |
121 | [Trait("Category", "UnitTest")]
122 | [Theory]
123 | [InlineData("This ")]
124 | [InlineData("This ", "is ")]
125 | [InlineData("This ", "is ", "a ")]
126 | [InlineData("This ", "is ", "a ", "test.")]
127 | public async Task Given_Valid_Messages_When_GetStreamingResponseAsync_Invoked_Then_It_Should_Call_ChatClient(params string[] responseMessages)
128 | {
129 | // Arrange
130 | IEnumerable responses = responseMessages.Select(m => new ChatResponseUpdate(ChatRole.Assistant, m));
131 |
132 | var chatClient = Substitute.For();
133 | chatClient.GetStreamingResponseAsync(Arg.Any>(), Arg.Any(), Arg.Any())
134 | .Returns(responses.ToAsyncEnumerable());
135 |
136 | var logger = Substitute.For>();
137 | var chatService = new ChatService(chatClient, logger);
138 |
139 | var messages = new List
140 | {
141 | new(ChatRole.System, "You are a helpful assistant."),
142 | new(ChatRole.User, "Why is the sky blue?")
143 | };
144 |
145 | // Act
146 | var result = chatService.GetStreamingResponseAsync(messages);
147 | var count = await result.CountAsync();
148 |
149 | // Assert
150 | result.ShouldNotBeNull();
151 | count.ShouldBe(responseMessages.Length);
152 | }
153 | }
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/TestConstants.cs:
--------------------------------------------------------------------------------
1 | namespace OpenChat.PlaygroundApp.Tests;
2 |
3 | public class TestConstants
4 | {
5 | public const string LocalhostUrl = "http://localhost:5280";
6 | }
7 |
--------------------------------------------------------------------------------
/test/OpenChat.PlaygroundApp.Tests/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "GitHubModels": {
3 | "Endpoint": "http://github-models/endpoint",
4 | "Token": "{{GITHUB_PAT}}",
5 | "Model": "github-models"
6 | }
7 | }
--------------------------------------------------------------------------------