Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ param githubModelsModel string = ''
@secure()
param githubModelsToken string = ''
// Google Vertex AI
param googleVertexAIModel string = ''
@secure()
param googleVertexAIApiKey string = ''
// Docker Model Runner
// Foundry Local
// Hugging Face
Expand Down Expand Up @@ -68,6 +71,8 @@ module resources 'resources.bicep' = {
connectorType: connectorType
githubModelsModel: githubModelsModel
githubModelsToken: githubModelsToken
googleVertexAIModel: googleVertexAIModel
googleVertexAIApiKey: googleVertexAIApiKey
huggingFaceModel: huggingFaceModel
openAIModel: openAIModel
openAIApiKey: openAIApiKey
Expand Down
6 changes: 6 additions & 0 deletions infra/main.parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
"githubModelsToken": {
"value": "${GH_MODELS_TOKEN}"
},
"googleVertexAIModel": {
"value": "${GOOGLE_VERTEX_AI_MODEL}"
},
"googleVertexAIApiKey": {
"value": "${GOOGLE_VERTEX_AI_API_KEY}"
},
"huggingFaceModel": {
"value": "${HUGGING_FACE_MODEL}"
},
Expand Down
14 changes: 14 additions & 0 deletions infra/resources.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ param githubModelsModel string = ''
@secure()
param githubModelsToken string = ''
// Google Vertex AI
param googleVertexAIModel string = ''
@secure()
param googleVertexAIApiKey string = ''
// Docker Model Runner
// Foundry Local
// Hugging Face
Expand Down Expand Up @@ -120,6 +123,17 @@ var envGitHubModels = (connectorType == '' || connectorType == 'GitHubModels') ?
}
] : []) : []
// Google Vertex AI
var envGoogleVertexAI = (connectorType == '' || connectorType == 'GoogleVertexAI') ? concat(googleVertexAIModel != '' ? [
{
name: 'GoogleVertexAI__Model'
value: googleVertexAIModel
}
] : [], googleVertexAIApiKey != '' ? [
{
name: 'GoogleVertexAI__ApiKey'
secretRef: 'google-vertex-ai-api-key'
}
] : []) : []
// Docker Model Runner
// Foundry Local
// Hugging Face
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public static async Task<IChatClient> CreateChatClientAsync(AppSettings settings
LanguageModelConnector connector = settings.ConnectorType switch
{
ConnectorType.GitHubModels => new GitHubModelsConnector(settings),
ConnectorType.GoogleVertexAI => new GoogleVertexAIConnector(settings),
ConnectorType.HuggingFace => new HuggingFaceConnector(settings),
ConnectorType.OpenAI => new OpenAIConnector(settings),
_ => throw new NotSupportedException($"Connector type '{settings.ConnectorType}' is not supported.")
Expand Down
51 changes: 51 additions & 0 deletions src/OpenChat.PlaygroundApp/Connectors/GoogleVertexAIConnector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.ClientModel;

using Microsoft.Extensions.AI;

using Mscc.GenerativeAI.Microsoft;

using OpenChat.PlaygroundApp.Abstractions;
using OpenChat.PlaygroundApp.Configurations;

namespace OpenChat.PlaygroundApp.Connectors;

/// <summary>
/// This represents the connector entity for Google Vertex AI.
/// </summary>
public class GoogleVertexAIConnector(AppSettings settings) : LanguageModelConnector(settings.GoogleVertexAI)
{
/// <inheritdoc/>
public override bool EnsureLanguageModelSettingsValid()
{
var settings = this.Settings as GoogleVertexAISettings;
if (settings is null)
{
throw new InvalidOperationException("Missing configuration: GoogleVertexAI.");
}

if (string.IsNullOrWhiteSpace(settings.ApiKey?.Trim()) == true)
{
throw new InvalidOperationException("Missing configuration: GoogleVertexAI:ApiKey.");
}

if (string.IsNullOrWhiteSpace(settings.Model?.Trim()) == true)
{
throw new InvalidOperationException("Missing configuration: GoogleVertexAI:Model.");
}

return true;
}

/// <inheritdoc/>
public override async Task<IChatClient> GetChatClientAsync()
{
var settings = this.Settings as GoogleVertexAISettings;

var apiKey = settings?.ApiKey ?? throw new InvalidOperationException("Missing configuration: GoogleVertexAI:ApiKey.");
var model = settings?.Model ?? throw new InvalidOperationException("Missing configuration: GoogleVertexAI:Model.");

var chatClient = new GeminiChatClient(apiKey, model);

return await Task.FromResult(chatClient).ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using Microsoft.Extensions.AI;

using OpenChat.PlaygroundApp.Abstractions;
using OpenChat.PlaygroundApp.Configurations;
using OpenChat.PlaygroundApp.Connectors;

namespace OpenChat.PlaygroundApp.Tests.Connectors;

public class GoogleVertexAIConnectorTests
{
private const string ApiKey = "AIzaSyA1234567890abcdefgHIJKLMNOpqrstuv";
private const string Model = "test-model";
private static AppSettings BuildAppSettings(string? apiKey = ApiKey, string? model = Model)
{
return new AppSettings
{
ConnectorType = ConnectorType.GoogleVertexAI,
GoogleVertexAI = new GoogleVertexAISettings
{
ApiKey = apiKey,
Model = model
}
};
}

[Trait("Category", "UnitTest")]
[Fact]
public void Given_Settings_Is_Null_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw()
{
// Arrange
var appSettings = new AppSettings { ConnectorType = ConnectorType.GoogleVertexAI, GoogleVertexAI = null };
var connector = new GoogleVertexAIConnector(appSettings);

// Act
Action action = () => connector.EnsureLanguageModelSettingsValid();

// Assert
action.ShouldThrow<InvalidOperationException>()
.Message.ShouldContain("GoogleVertexAI");
}

[Trait("Category", "UnitTest")]
[Theory]
[InlineData(null, typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")]
[InlineData("", typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")]
[InlineData(" ", typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")]
public void Given_Invalid_ApiKey_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? apiKey, Type expectedType, string expectedMessage)

Check warning on line 47 in test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs

View workflow job for this annotation

GitHub Actions / build-test

Theory method 'Given_Invalid_ApiKey_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw' on test class 'GoogleVertexAIConnectorTests' does not use parameter 'expectedType'. Use the parameter, or remove the parameter and associated data. (https://xunit.net/xunit.analyzers/rules/xUnit1026)

Check warning on line 47 in test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs

View workflow job for this annotation

GitHub Actions / build-test

Theory method 'Given_Invalid_ApiKey_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw' on test class 'GoogleVertexAIConnectorTests' does not use parameter 'expectedType'. Use the parameter, or remove the parameter and associated data. (https://xunit.net/xunit.analyzers/rules/xUnit1026)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type expectedType 매개인자는 사용하지 않으니 모두 제거해주세요
다른 테스트 메서드도 모두 동일합니다 ~

{
// Arrange
var appSettings = BuildAppSettings(apiKey: apiKey);
var connector = new GoogleVertexAIConnector(appSettings);

// Act
Action action = () => connector.EnsureLanguageModelSettingsValid();

// Assert
action.ShouldThrow<InvalidOperationException>()
.Message.ShouldContain(expectedMessage);
}

[Trait("Category", "UnitTest")]
[Theory]
[InlineData(null, typeof(InvalidOperationException), "GoogleVertexAI:Model")]
[InlineData("", typeof(InvalidOperationException), "GoogleVertexAI:Model")]
[InlineData(" ", typeof(InvalidOperationException), "GoogleVertexAI:Model")]
public void Given_Invalid_Model_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? model, Type expectedType, string expectedMessage)

Check warning on line 66 in test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs

View workflow job for this annotation

GitHub Actions / build-test

Theory method 'Given_Invalid_Model_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw' on test class 'GoogleVertexAIConnectorTests' does not use parameter 'expectedType'. Use the parameter, or remove the parameter and associated data. (https://xunit.net/xunit.analyzers/rules/xUnit1026)

Check warning on line 66 in test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs

View workflow job for this annotation

GitHub Actions / build-test

Theory method 'Given_Invalid_Model_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw' on test class 'GoogleVertexAIConnectorTests' does not use parameter 'expectedType'. Use the parameter, or remove the parameter and associated data. (https://xunit.net/xunit.analyzers/rules/xUnit1026)
{
// Arrange
var appSettings = BuildAppSettings(apiKey: "valid-key", model: model);
var connector = new GoogleVertexAIConnector(appSettings);

// Act
Action action = () => connector.EnsureLanguageModelSettingsValid();

// Assert
action.ShouldThrow<InvalidOperationException>()
.Message.ShouldContain(expectedMessage);
}

[Trait("Category", "UnitTest")]
[Fact]
public void Given_Valid_Settings_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Return_True()
{
// Arrange
var appSettings = BuildAppSettings();
var connector = new GoogleVertexAIConnector(appSettings);

// Act
var result = connector.EnsureLanguageModelSettingsValid();

// Assert
result.ShouldBeTrue();
}

[Trait("Category", "UnitTest")]
[Fact]
public async Task Given_Valid_Settings_When_GetChatClient_Invoked_Then_It_Should_Return_ChatClient()
{
var settings = BuildAppSettings();
var connector = new GoogleVertexAIConnector(settings);

var client = await connector.GetChatClientAsync();

client.ShouldNotBeNull();
}

[Trait("Category", "UnitTest")]
[Fact]
public void Given_Settings_Is_Null_When_GetChatClientAsync_Invoked_Then_It_Should_Throw()
{
// Arrange
var appSettings = new AppSettings { ConnectorType = ConnectorType.GoogleVertexAI, GoogleVertexAI = null };
var connector = new GoogleVertexAIConnector(appSettings);

// Act
Func<Task> func = async () => await connector.GetChatClientAsync();

// Assert
func.ShouldThrow<InvalidOperationException>();
}

[Trait("Category", "UnitTest")]
[Theory]
[InlineData(null, typeof(InvalidOperationException), "GoogleVertexAI:ApiKey")]
[InlineData("", typeof(ArgumentException), "key")]
public async Task Given_Missing_ApiKey_When_GetChatClient_Invoked_Then_It_Should_Throw(string? apiKey, Type expected, string message)

Check warning on line 126 in test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs

View workflow job for this annotation

GitHub Actions / build-test

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 126 in test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs

View workflow job for this annotation

GitHub Actions / build-test

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
// Arrange
var settings = BuildAppSettings(apiKey: apiKey);
var connector = new GoogleVertexAIConnector(settings);

// Act
Func<Task> func = async () => await connector.GetChatClientAsync();

// Assert
func.ShouldThrow(expected)
.Message.ShouldContain(message);
}

[Trait("Category", "UnitTest")]
[Theory]
[InlineData(null, typeof(InvalidOperationException), "model")]
public async Task Given_Missing_Model_When_GetChatClient_Invoked_Then_It_Should_Throw(string? model, Type expected, string message)

Check warning on line 143 in test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs

View workflow job for this annotation

GitHub Actions / build-test

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 143 in test/OpenChat.PlaygroundApp.Tests/Connectors/GoogleVertexAIConnectorTests.cs

View workflow job for this annotation

GitHub Actions / build-test

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
// Arrange
var settings = BuildAppSettings(model: model);
var connector = new GoogleVertexAIConnector(settings);

// Act
Func<Task> func = async () => await connector.GetChatClientAsync();

// Assert
func.ShouldThrow(expected)
.Message.ShouldContain(message);
}

[Trait("Category", "UnitTest")]
[Fact]
public async Task Given_Valid_Settings_When_CreateChatClientAsync_Invoked_Then_It_Should_Return_ChatClient()
{
// Arrange
var settings = BuildAppSettings();

// Act
var result = await LanguageModelConnector.CreateChatClientAsync(settings);

// Assert
result.ShouldNotBeNull();
result.ShouldBeAssignableTo<IChatClient>();
}

[Trait("Category", "UnitTest")]
[Fact]
public void Given_Invalid_Settings_When_CreateChatClientAsync_Invoked_Then_It_Should_Throw()
{
// Arrange
var settings = new AppSettings
{
ConnectorType = ConnectorType.GoogleVertexAI,
GoogleVertexAI = new GoogleVertexAISettings
{
ApiKey = null,
Model = "test-model"
}
};

// Act
Func<Task> func = async () => await LanguageModelConnector.CreateChatClientAsync(settings);

// Assert
func.ShouldThrow<InvalidOperationException>();
}
}