diff --git a/README.md b/README.md index a7e59fa..f53c7df 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ The package supports the following use cases to authenticate. - Google AI: [Authentication with an API key](https://ai.google.dev/tutorials/setup) - Google AI: [Authentication with OAuth](https://ai.google.dev/docs/oauth_quickstart) - Vertex AI: [Authentication with Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/provide-credentials-adc#local-dev) -- Vertex AI: [Authentication with OAuth 2.0]() (using [Mscc.GenerativeAI.Google](./src/Mscc.GenerativeAI.Google)) +- Vertex AI: [Authentication with OAuth]() (using [Mscc.GenerativeAI.Google](./src/Mscc.GenerativeAI.Google)) - Vertex AI: [Authentication with Service Account]() (using [Mscc.GenerativeAI.Google](./src/Mscc.GenerativeAI.Google)) This applies mainly to the instantiation procedure. @@ -62,6 +62,28 @@ This applies mainly to the instantiation procedure. Use of Gemini API in either Google AI or Vertex AI is almost identical. The major difference is the way to instantiate the model handling your prompt. +### Using Environment variables + +In the cloud most settings are configured via environment variables (EnvVars). The ease of configuration, their wide spread support and the simplicity of environment variables makes them a very interesting option. + +| Variable Name | Description | +|--------------------------------|---------------------------------------------------------------| +| GOOGLE_AI_MODEL | The name of the model to use (default is *Model.Gemini10Pro*) | +| GOOGLE_API_KEY | The API key generated in Google AI Studio | +| GOOGLE_PROJECT_ID | Project ID in Google Cloud to access the APIs | +| GOOGLE_REGION | Region in Google Cloud (default is *us-central1*) | +| GOOGLE_ACCESS_TOKEN | The access token required to use models running in Vertex AI | +| GOOGLE_APPLICATION_CREDENTIALS | Path to the application credentials file. | +| GOOGLE_WEB_CREDENTIALS | Path to a Web credentials file. | + +Using any environment variable provides a simplified access to a model. + +```csharp +using Mscc.GenerativeAI; + +var model = new GenerativeModel(); +``` + ### Choose an API and authentication mode Google AI with an API key diff --git a/src/Mscc.GenerativeAI/GenerativeModel.cs b/src/Mscc.GenerativeAI/GenerativeModel.cs index b5f9e7b..07a7499 100644 --- a/src/Mscc.GenerativeAI/GenerativeModel.cs +++ b/src/Mscc.GenerativeAI/GenerativeModel.cs @@ -27,15 +27,16 @@ public class GenerativeModel private const string MediaType = "application/json"; private readonly bool _useVertexAi; - private readonly bool _useHeaderApiKey; - private readonly bool _useHeaderProjectId; private readonly string _model; - private readonly string? _apiKey; - private readonly string? _projectId; - private readonly string? _region; + private readonly string _region = "us-central1"; private readonly string _publisher = "google"; private readonly JsonSerializerOptions _options; + + private bool _useHeaderApiKey; + private string? _apiKey; private string? _accessToken; + private bool _useHeaderProjectId; + private string? _projectId; private List? _safetySettings; private GenerationConfig? _generationConfig; private List? _tools; @@ -117,9 +118,25 @@ private string Method /// Name of the model. public string Name => _model; + public string? ApiKey + { + set + { + _apiKey = value; + if (!string.IsNullOrEmpty(_apiKey)) + { + _useHeaderApiKey = Client.DefaultRequestHeaders.Contains("x-goog-api-key"); + if (!_useHeaderApiKey) + { + Client.DefaultRequestHeaders.Add("x-goog-api-key", _apiKey); + } + _useHeaderApiKey = Client.DefaultRequestHeaders.Contains("x-goog-api-key"); + } + } + } + public string? AccessToken { - get => _accessToken; set { _accessToken = value; @@ -127,38 +144,48 @@ public string? AccessToken Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken); } } + + public string? ProjectId + { + set + { + _projectId = value; + if (!string.IsNullOrEmpty(_projectId)) + { + _useHeaderProjectId = Client.DefaultRequestHeaders.Contains("x-goog-user-project"); + if (!_useHeaderProjectId) + { + Client.DefaultRequestHeaders.Add("x-goog-user-project", _projectId); + } + _useHeaderProjectId = Client.DefaultRequestHeaders.Contains("x-goog-user-project"); + } + } + } - // Todo: Integrate Google.Apis.Auth to retrieve Access_Token on demand. - // Todo: Integrate Application Default Credentials as an alternative. - // Reference: https://cloud.google.com/docs/authentication + /// + /// Default constructor attempts to read environment variables and + /// sets default values, if available + /// public GenerativeModel() { _options = DefaultJsonSerializerOptions(); - _model = Environment.GetEnvironmentVariable("GOOGLE_AI_MODEL") ?? - Model.Gemini10Pro; - _apiKey = Environment.GetEnvironmentVariable("GOOGLE_API_KEY"); - _projectId = Environment.GetEnvironmentVariable("GOOGLE_PROJECT_ID"); - _region = Environment.GetEnvironmentVariable("GOOGLE_REGION"); - AccessToken = Environment.GetEnvironmentVariable("GOOGLE_ACCESS_TOKEN") ?? - GetAccessTokenFromAdc(); - + GenerativeModelExtensions.ReadDotEnv(); var credentialsFile = Environment.GetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS") ?? Environment.GetEnvironmentVariable("GOOGLE_WEB_CREDENTIALS") ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "gcloud", - "application_default_credentials.json"); - if (File.Exists(credentialsFile)) - { - using (var stream = new FileStream(credentialsFile, FileMode.Open, FileAccess.Read)) - { - var json = JsonSerializer.DeserializeAsync(stream, _options).Result; - _projectId ??= json.GetValue("quota_project_id") ?? - json.GetValue("project_id"); - } - } //var credentials = GoogleCredential.FromFile() + "application_default_credentials.json"); + var credentials = GetCredentialsFromFile(credentialsFile); + + ApiKey = Environment.GetEnvironmentVariable("GOOGLE_API_KEY"); + AccessToken = Environment.GetEnvironmentVariable("GOOGLE_ACCESS_TOKEN"); + ProjectId = Environment.GetEnvironmentVariable("GOOGLE_PROJECT_ID") ?? + credentials?.ProjectId; + _model = Environment.GetEnvironmentVariable("GOOGLE_AI_MODEL") ?? + Model.Gemini10Pro; + _region = Environment.GetEnvironmentVariable("GOOGLE_REGION") ?? _region; } - // Todo: Add parameters for GenerationConfig, SafetySettings, Transport? and Tools /// /// Constructor to initialize access to Google AI Gemini API. /// @@ -171,20 +198,10 @@ public GenerativeModel(string? apiKey = null, GenerationConfig? generationConfig = null, List? safetySettings = null) : this() { - _apiKey = apiKey ?? _apiKey; - _model = model.SanitizeModelName() ?? _model; + ApiKey = apiKey ?? _apiKey; + _model = model?.SanitizeModelName() ?? _model; _generationConfig ??= generationConfig; _safetySettings ??= safetySettings; - - if (!string.IsNullOrEmpty(apiKey)) - { - _useHeaderApiKey = Client.DefaultRequestHeaders.Contains("x-goog-api-key"); - if (!_useHeaderApiKey) - { - Client.DefaultRequestHeaders.Add("x-goog-api-key", _apiKey); - } - _useHeaderApiKey = Client.DefaultRequestHeaders.Contains("x-goog-api-key"); - } } /// @@ -195,27 +212,19 @@ public GenerativeModel(string? apiKey = null, /// Model to use /// /// - internal GenerativeModel(string projectId, string region, - string model = Model.Gemini10Pro, + internal GenerativeModel(string? projectId = null, string? region = null, + string? model = null, GenerationConfig? generationConfig = null, List? safetySettings = null) : this() { _useVertexAi = true; - _projectId = projectId; - _region = region; - _model = model.SanitizeModelName(); + AccessToken = Environment.GetEnvironmentVariable("GOOGLE_ACCESS_TOKEN") ?? + GetAccessTokenFromAdc(); + ProjectId = projectId ?? _projectId; + _region = region ?? _region; + _model = model?.SanitizeModelName() ?? _model; _generationConfig = generationConfig; _safetySettings = safetySettings; - - if (!string.IsNullOrEmpty(projectId)) - { - _useHeaderProjectId = Client.DefaultRequestHeaders.Contains("x-goog-user-project"); - if (!_useHeaderProjectId) - { - Client.DefaultRequestHeaders.Add("x-goog-user-project", _projectId); - } - _useHeaderProjectId = Client.DefaultRequestHeaders.Contains("x-goog-user-project"); - } } /// @@ -643,6 +652,32 @@ internal JsonSerializerOptions DefaultJsonSerializerOptions() return options; } + /// + /// + /// + /// + /// + private Credentials? GetCredentialsFromFile(string credentialsFile) + { + Credentials? credentials = null; + if (File.Exists(credentialsFile)) + { + var options = DefaultJsonSerializerOptions(); + options.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; + using (var stream = new FileStream(credentialsFile, FileMode.Open, FileAccess.Read)) + { + credentials = JsonSerializer.Deserialize(stream, options); + } + } + + return credentials; + } + + /// + /// Retrieve access token from Application Default Credentials (ADC) + /// + /// The access token. + // Reference: https://cloud.google.com/docs/authentication private string GetAccessTokenFromAdc() { if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) diff --git a/src/Mscc.GenerativeAI/GenerativeModelExtensions.cs b/src/Mscc.GenerativeAI/GenerativeModelExtensions.cs index f6ce17a..b946767 100644 --- a/src/Mscc.GenerativeAI/GenerativeModelExtensions.cs +++ b/src/Mscc.GenerativeAI/GenerativeModelExtensions.cs @@ -1,5 +1,6 @@ #if NET472_OR_GREATER || NETSTANDARD2_0 using System; +using System.IO; using System.Linq; using System.Text.Json; #endif @@ -32,5 +33,19 @@ public static class GenerativeModelExtensions return result; } + + public static void ReadDotEnv(string dotEnvFile = ".env") + { + if (!File.Exists(dotEnvFile)) return; + + foreach (var line in File.ReadAllLines(dotEnvFile)) + { + var parts = line.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length != 2) continue; + + Environment.SetEnvironmentVariable(parts[0], parts[1]); + } + } } } \ No newline at end of file diff --git a/src/Mscc.GenerativeAI/Mscc.GenerativeAI.csproj b/src/Mscc.GenerativeAI/Mscc.GenerativeAI.csproj index 73840fe..04f48a9 100644 --- a/src/Mscc.GenerativeAI/Mscc.GenerativeAI.csproj +++ b/src/Mscc.GenerativeAI/Mscc.GenerativeAI.csproj @@ -23,6 +23,7 @@ False - use EnvVars for default values (parameterless constructor) + - support of .env file - improve Function Calling - improve Chat streaming - improve Embeddings diff --git a/src/Mscc.GenerativeAI/RELEASE.md b/src/Mscc.GenerativeAI/RELEASE.md index e4969ef..23e85dd 100644 --- a/src/Mscc.GenerativeAI/RELEASE.md +++ b/src/Mscc.GenerativeAI/RELEASE.md @@ -1,5 +1,13 @@ # Release Notes +## 0.7.0 + +- use Environment Variables for default values (parameterless constructor) +- support of .env file +- improve Function Calling +- improve Chat streaming +- improve Embeddings + ## 0.6.1 - implement Function Calling diff --git a/src/Mscc.GenerativeAI/Types/Credentials.cs b/src/Mscc.GenerativeAI/Types/Credentials.cs new file mode 100644 index 0000000..fd23397 --- /dev/null +++ b/src/Mscc.GenerativeAI/Types/Credentials.cs @@ -0,0 +1,37 @@ +namespace Mscc.GenerativeAI +{ + public class Credentials : ClientSecrets + { + private string _projectId; + + public ClientSecrets Web { get; set; } + public ClientSecrets Installed { get; set; } + + public string Account { get; set; } + public string RefreshToken { get; set; } + public string Type { get; set; } + public string UniverseDomain { get; set; } + + public string ProjectId + { + get => _projectId; + set => _projectId = value; + } + + public virtual string QuotaProjectId + { + get => _projectId; + set => _projectId = value; + } + } + + public class ClientSecrets + { + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string[] RedirectUris { get; set; } + public string AuthUri { get; set; } + public string AuthProviderX509CertUrl { get; set; } + public string TokenUri { get; set; } + } +} \ No newline at end of file diff --git a/tests/Mscc.GenerativeAI.Google/ConfigurationFixture.cs b/tests/Mscc.GenerativeAI.Google/ConfigurationFixture.cs index e366c80..dbf38e6 100644 --- a/tests/Mscc.GenerativeAI.Google/ConfigurationFixture.cs +++ b/tests/Mscc.GenerativeAI.Google/ConfigurationFixture.cs @@ -26,6 +26,8 @@ public class ConfigurationFixture : ICollectionFixture // Ref: https://cloud.google.com/vertex-ai/docs/start/client-libraries public ConfigurationFixture() { + ReadDotEnv(); + Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: true) .AddJsonFile("appsettings.user.json", optional: true) @@ -126,5 +128,19 @@ private string Format(string filename, string arguments) ((string.IsNullOrEmpty(arguments)) ? string.Empty : " " + arguments) + "'"; } + + private void ReadDotEnv(string dotEnvFile = ".env") + { + if (!File.Exists(dotEnvFile)) return; + + foreach (var line in File.ReadAllLines(dotEnvFile)) + { + var parts = line.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length != 2) continue; + + Environment.SetEnvironmentVariable(parts[0], parts[1]); + } + } } } \ No newline at end of file diff --git a/tests/Mscc.GenerativeAI/ConfigurationFixture.cs b/tests/Mscc.GenerativeAI/ConfigurationFixture.cs index 9e11b7b..a2208e1 100644 --- a/tests/Mscc.GenerativeAI/ConfigurationFixture.cs +++ b/tests/Mscc.GenerativeAI/ConfigurationFixture.cs @@ -25,6 +25,8 @@ public class ConfigurationFixture : ICollectionFixture // Ref: https://cloud.google.com/vertex-ai/docs/start/client-libraries public ConfigurationFixture() { + ReadDotEnv(); + Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: true) .AddJsonFile("appsettings.user.json", optional: true) @@ -120,5 +122,19 @@ private string Format(string filename, string arguments) ((string.IsNullOrEmpty(arguments)) ? string.Empty : " " + arguments) + "'"; } + + private void ReadDotEnv(string dotEnvFile = ".env") + { + if (!File.Exists(dotEnvFile)) return; + + foreach (var line in File.ReadAllLines(dotEnvFile)) + { + var parts = line.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length != 2) continue; + + Environment.SetEnvironmentVariable(parts[0], parts[1]); + } + } } } \ No newline at end of file diff --git a/tests/Mscc.GenerativeAI/GoogleAi_GeminiPro_Should.cs b/tests/Mscc.GenerativeAI/GoogleAi_GeminiPro_Should.cs index af161d8..7d338e9 100644 --- a/tests/Mscc.GenerativeAI/GoogleAi_GeminiPro_Should.cs +++ b/tests/Mscc.GenerativeAI/GoogleAi_GeminiPro_Should.cs @@ -27,39 +27,42 @@ public void Initialize_EnvVars() { // Arrange Environment.SetEnvironmentVariable("GOOGLE_API_KEY", fixture.ApiKey); + var expected = Environment.GetEnvironmentVariable("GOOGLE_AI_MODEL") ?? Model.Gemini10Pro; // Act var model = new GenerativeModel(); // Assert model.Should().NotBeNull(); - model.Name.Should().Be(Model.Gemini10Pro); + model.Name.Should().Be(expected); } [Fact] public void Initialize_Default_Model() { // Arrange + var expected = Environment.GetEnvironmentVariable("GOOGLE_AI_MODEL") ?? Model.Gemini10Pro; // Act var model = new GenerativeModel(apiKey: fixture.ApiKey); // Assert model.Should().NotBeNull(); - model.Name.Should().Be(Model.Gemini10Pro); + model.Name.Should().Be(expected); } [Fact] public void Initialize_Model() { // Arrange + var expected = this.model; // Act var model = new GenerativeModel(apiKey: fixture.ApiKey, model: this.model); // Assert model.Should().NotBeNull(); - model.Name.Should().Be(Model.Gemini10Pro); + model.Name.Should().Be(expected); } [Fact]