Skip to content

Commit

Permalink
support for envVars and .env; read credentials file
Browse files Browse the repository at this point in the history
  • Loading branch information
jochenkirstaetter committed Mar 15, 2024
1 parent 858c5d6 commit 7594ad9
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 59 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
145 changes: 90 additions & 55 deletions src/Mscc.GenerativeAI/GenerativeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SafetySetting>? _safetySettings;
private GenerationConfig? _generationConfig;
private List<Tool>? _tools;
Expand Down Expand Up @@ -117,48 +118,74 @@ private string Method
/// <returns>Name of the model.</returns>
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;
if (value != null)
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
/// <summary>
/// Default constructor attempts to read environment variables and
/// sets default values, if available
/// </summary>
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<JsonElement>(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
/// <summary>
/// Constructor to initialize access to Google AI Gemini API.
/// </summary>
Expand All @@ -171,20 +198,10 @@ public GenerativeModel(string? apiKey = null,
GenerationConfig? generationConfig = null,
List<SafetySetting>? 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");
}
}

/// <summary>
Expand All @@ -195,27 +212,19 @@ public GenerativeModel(string? apiKey = null,
/// <param name="model">Model to use</param>
/// <param name="generationConfig"></param>
/// <param name="safetySettings"></param>
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<SafetySetting>? 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");
}
}

/// <summary>
Expand Down Expand Up @@ -643,6 +652,32 @@ internal JsonSerializerOptions DefaultJsonSerializerOptions()
return options;
}

/// <summary>
///
/// </summary>
/// <param name="credentialsFile"></param>
/// <returns></returns>
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<Credentials>(stream, options);
}
}

return credentials;
}

/// <summary>
/// Retrieve access token from Application Default Credentials (ADC)
/// </summary>
/// <returns>The access token.</returns>
// Reference: https://cloud.google.com/docs/authentication
private string GetAccessTokenFromAdc()
{
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
Expand Down
15 changes: 15 additions & 0 deletions src/Mscc.GenerativeAI/GenerativeModelExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#if NET472_OR_GREATER || NETSTANDARD2_0
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
#endif
Expand Down Expand Up @@ -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]);
}
}
}
}
1 change: 1 addition & 0 deletions src/Mscc.GenerativeAI/Mscc.GenerativeAI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<PackageRequireLicenseAcceptance>False</PackageRequireLicenseAcceptance>
<PackageReleaseNotes>
- use EnvVars for default values (parameterless constructor)
- support of .env file
- improve Function Calling
- improve Chat streaming
- improve Embeddings
Expand Down
8 changes: 8 additions & 0 deletions src/Mscc.GenerativeAI/RELEASE.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
37 changes: 37 additions & 0 deletions src/Mscc.GenerativeAI/Types/Credentials.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
16 changes: 16 additions & 0 deletions tests/Mscc.GenerativeAI.Google/ConfigurationFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class ConfigurationFixture : ICollectionFixture<ConfigurationFixture>
// 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)
Expand Down Expand Up @@ -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]);
}
}
}
}
16 changes: 16 additions & 0 deletions tests/Mscc.GenerativeAI/ConfigurationFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public class ConfigurationFixture : ICollectionFixture<ConfigurationFixture>
// 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)
Expand Down Expand Up @@ -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]);
}
}
}
}
Loading

0 comments on commit 7594ad9

Please sign in to comment.