diff --git a/.gitignore b/.gitignore
index 07e53ed..c3e7b71 100644
--- a/.gitignore
+++ b/.gitignore
@@ -396,3 +396,4 @@ FodyWeavers.xsd
**/appsettings.Development.json
**/client_secret*.json
*.p12
+*.DotSettings
diff --git a/README.md b/README.md
index 216a7f4..86c6205 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ Alternatively, add the following line to your `.csproj` file.
```text
-
+
```
diff --git a/VERSION b/VERSION
index afa2b35..b9268da 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.8.0
\ No newline at end of file
+1.8.1
\ No newline at end of file
diff --git a/src/Mscc.GenerativeAI.Google/CHANGELOG.md b/src/Mscc.GenerativeAI.Google/CHANGELOG.md
index 03a8a09..64c2bf5 100644
--- a/src/Mscc.GenerativeAI.Google/CHANGELOG.md
+++ b/src/Mscc.GenerativeAI.Google/CHANGELOG.md
@@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
### Fixed
+## 1.8.1
+
+### Changed
+
+- bump version
+
## 1.8.0
### Changed
diff --git a/src/Mscc.GenerativeAI.Web/CHANGELOG.md b/src/Mscc.GenerativeAI.Web/CHANGELOG.md
index ab8287d..ff5950a 100644
--- a/src/Mscc.GenerativeAI.Web/CHANGELOG.md
+++ b/src/Mscc.GenerativeAI.Web/CHANGELOG.md
@@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
### Fixed
+## 1.8.1
+
+### Changed
+
+- bump version
+
## 1.8.0
### Changed
diff --git a/src/Mscc.GenerativeAI/BaseGeneration.cs b/src/Mscc.GenerativeAI/BaseModel.cs
similarity index 90%
rename from src/Mscc.GenerativeAI/BaseGeneration.cs
rename to src/Mscc.GenerativeAI/BaseModel.cs
index 872ff6c..11206c1 100644
--- a/src/Mscc.GenerativeAI/BaseGeneration.cs
+++ b/src/Mscc.GenerativeAI/BaseModel.cs
@@ -8,6 +8,7 @@
using System.Text.Json.Serialization;
using System.Threading.Tasks;
#endif
+using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers;
@@ -16,19 +17,18 @@
namespace Mscc.GenerativeAI
{
- public abstract class BaseGeneration
+ public abstract class BaseModel : BaseLogger
{
- private const string EndpointGoogleAi = "https://generativelanguage.googleapis.com";
+ protected const string EndpointGoogleAi = "https://generativelanguage.googleapis.com";
- protected readonly string _region = "us-central1";
protected readonly string _publisher = "google";
protected readonly JsonSerializerOptions _options;
- internal readonly Credentials? _credentials;
protected string _model;
protected string? _apiKey;
protected string? _accessToken;
protected string? _projectId;
+ protected string _region = "us-central1";
#if NET472_OR_GREATER || NETSTANDARD2_0
protected static readonly Version _httpVersion = HttpVersion.Version11;
@@ -85,6 +85,19 @@ public string? ApiKey
}
}
+ ///
+ /// Sets the access token to use for the request.
+ ///
+ public string? AccessToken
+ {
+ set
+ {
+ _accessToken = value;
+ if (value != null)
+ Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
+ }
+ }
+
///
/// Sets the project ID to use for the request.
///
@@ -106,18 +119,14 @@ public string? ProjectId
}
}
}
-
+
///
- /// Sets the access token to use for the request.
+ /// Returns the region to use for the request.
///
- public string? AccessToken
+ public string Region
{
- set
- {
- _accessToken = value;
- if (value != null)
- Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
- }
+ get => _region;
+ set => _region = value;
}
///
@@ -129,17 +138,14 @@ public TimeSpan Timeout
set => Client.Timeout = value;
}
- public BaseGeneration()
+ ///
+ ///
+ ///
+ /// Optional. Logger instance used for logging
+ public BaseModel(ILogger? logger = null) : base(logger)
{
_options = DefaultJsonSerializerOptions();
GenerativeAIExtensions.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");
- _credentials = GetCredentialsFromFile(credentialsFile);
-
ApiKey = Environment.GetEnvironmentVariable("GOOGLE_API_KEY");
AccessToken = Environment.GetEnvironmentVariable("GOOGLE_ACCESS_TOKEN"); // ?? GetAccessTokenFromAdc();
Model = Environment.GetEnvironmentVariable("GOOGLE_AI_MODEL") ??
@@ -147,14 +153,27 @@ public BaseGeneration()
_region = Environment.GetEnvironmentVariable("GOOGLE_REGION") ?? _region;
}
- public BaseGeneration(string? projectId = null, string? region = null,
- string? model = null) : this()
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Optional. Logger instance used for logging
+ public BaseModel(string? projectId = null, string? region = null,
+ string? model = null, ILogger? logger = null) : this(logger)
{
- AccessToken = Environment.GetEnvironmentVariable("GOOGLE_ACCESS_TOKEN") ??
+ var credentialsFile =
+ Environment.GetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS") ??
+ Environment.GetEnvironmentVariable("GOOGLE_WEB_CREDENTIALS") ??
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "gcloud",
+ "application_default_credentials.json");
+ var credentials = GetCredentialsFromFile(credentialsFile);
+ AccessToken = _accessToken ??
GetAccessTokenFromAdc();
ProjectId = projectId ??
Environment.GetEnvironmentVariable("GOOGLE_PROJECT_ID") ??
- _credentials?.ProjectId ??
+ credentials?.ProjectId ??
_projectId;
_region = region ?? _region;
Model = model ?? _model;
@@ -318,7 +337,7 @@ private string RunExternalExe(string filename, string arguments)
}
catch (Exception e)
{
- // throw new Exception("OS error while executing " + Format(filename, arguments)+ ": " + e.Message, e);
+ Logger.LogRunExternalExe("OS error while executing " + Format(filename, arguments)+ ": " + e.Message);
return string.Empty;
}
diff --git a/src/Mscc.GenerativeAI/CHANGELOG.md b/src/Mscc.GenerativeAI/CHANGELOG.md
index 018ad12..eb3c5ea 100644
--- a/src/Mscc.GenerativeAI/CHANGELOG.md
+++ b/src/Mscc.GenerativeAI/CHANGELOG.md
@@ -10,7 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Feature suggestion: Retry mechanism ([#2](https://github.com/mscraftsman/generative-ai/issues/2))
-- Feature suggestion: Add logs with LogLevel using the Standard logging in .NET ([#6](https://github.com/mscraftsman/generative-ai/issues/6))
- implement Automatic Function Call (AFC)
### Changed
@@ -18,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## 1.8.1
+### Added
+
+- add logs with LogLevel using the Standard logging in .NET ([#6](https://github.com/mscraftsman/generative-ai/issues/6)) - thanks @doggy8088
+
### Changed
- improve regarding XMLdoc, typos, and non-nullable properties
diff --git a/src/Mscc.GenerativeAI/CachedContentModel.cs b/src/Mscc.GenerativeAI/CachedContentModel.cs
index 7ff13d3..17da2b0 100644
--- a/src/Mscc.GenerativeAI/CachedContentModel.cs
+++ b/src/Mscc.GenerativeAI/CachedContentModel.cs
@@ -6,6 +6,7 @@
using System.Threading;
using System.Threading.Tasks;
#endif
+using Microsoft.Extensions.Logging;
using System.Text;
namespace Mscc.GenerativeAI
@@ -14,17 +15,20 @@ namespace Mscc.GenerativeAI
/// Content that has been preprocessed and can be used in subsequent request to GenerativeService.
/// Cached content can be only used with model it was created for.
///
- public class CachedContentModel : BaseGeneration
+ public sealed class CachedContentModel : BaseModel
{
protected override string Version => ApiVersion.V1Beta;
- private const string EndpointGoogleAi = "https://generativelanguage.googleapis.com";
///
- ///
+ /// Initializes a new instance of the class.
///
- public CachedContentModel()
- {
- }
+ public CachedContentModel() : this(logger: null) { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Optional. Logger instance used for logging
+ public CachedContentModel(ILogger? logger = null) : base(logger) { }
///
/// Creates CachedContent resource.
diff --git a/src/Mscc.GenerativeAI/FilesModel.cs b/src/Mscc.GenerativeAI/FilesModel.cs
index 8955670..7aef390 100644
--- a/src/Mscc.GenerativeAI/FilesModel.cs
+++ b/src/Mscc.GenerativeAI/FilesModel.cs
@@ -1,22 +1,27 @@
#if NET472_OR_GREATER || NETSTANDARD2_0
using System;
using System.Collections.Generic;
-using System.IO;
-using System.Linq;
using System.Net.Http;
-using System.Security.Authentication;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using System.Threading;
using System.Threading.Tasks;
#endif
+using Microsoft.Extensions.Logging;
namespace Mscc.GenerativeAI
{
- public class FilesModel : BaseGeneration
+ public sealed class FilesModel : BaseModel
{
protected override string Version => ApiVersion.V1Beta;
- private const string EndpointGoogleAi = "https://generativelanguage.googleapis.com";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FilesModel() : this(logger: null) { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Optional. Logger instance used for logging
+ public FilesModel(ILogger? logger) : base(logger) { }
///
/// Lists the metadata for Files owned by the requesting project.
diff --git a/src/Mscc.GenerativeAI/GenerativeModel.cs b/src/Mscc.GenerativeAI/GenerativeModel.cs
index 69803c8..035c51b 100644
--- a/src/Mscc.GenerativeAI/GenerativeModel.cs
+++ b/src/Mscc.GenerativeAI/GenerativeModel.cs
@@ -4,63 +4,32 @@
using System.IO;
using System.Linq;
using System.Net.Http;
-using System.Security.Authentication;
using System.Text.Json;
-using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
#endif
-using System.Diagnostics;
-using System.Net;
+using Microsoft.Extensions.Logging;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.CompilerServices;
-using System.Text.RegularExpressions;
using System.Text;
namespace Mscc.GenerativeAI
{
- public class GenerativeModel
+ public class GenerativeModel : BaseModel
{
- private const string EndpointGoogleAi = "https://generativelanguage.googleapis.com";
private const string UrlGoogleAi = "{endpointGoogleAI}/{version}/{model}:{method}";
private const string UrlVertexAi = "https://{region}-aiplatform.googleapis.com/{version}/projects/{projectId}/locations/{region}/publishers/{publisher}/{model}:{method}";
private readonly bool _useVertexAi;
- private readonly string _publisher = "google";
- private readonly JsonSerializerOptions _options;
private readonly CachedContent? _cachedContent;
- private string _model;
- private string? _apiKey;
- private string? _accessToken;
- private string? _projectId;
- private string _region = "us-central1";
private List? _safetySettings;
private GenerationConfig? _generationConfig;
private List? _tools;
private ToolConfig? _toolConfig;
private Content? _systemInstruction;
-#if NET472_OR_GREATER || NETSTANDARD2_0
- private static readonly Version _httpVersion = HttpVersion.Version11;
- private static readonly HttpClient Client = new HttpClient(new HttpClientHandler
- {
- SslProtocols = SslProtocols.Tls12
- });
-#else
- private static readonly Version _httpVersion = HttpVersion.Version11;
- private static readonly HttpClient Client = new HttpClient(new SocketsHttpHandler
- {
- PooledConnectionLifetime = TimeSpan.FromMinutes(30),
- EnableMultipleHttp2Connections = true
- })
- {
- DefaultRequestVersion = _httpVersion,
- DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher
- };
-#endif
-
private string Url
{
get
@@ -75,7 +44,7 @@ private string Url
}
}
- private string Version
+ protected override string Version
{
get
{
@@ -117,83 +86,6 @@ private string Method
internal bool IsVertexAI => _useVertexAi;
- private string Model
- {
- set => _model = value.SanitizeModelName();
- }
-
- ///
- /// Sets the API key to use for the request.
- ///
- ///
- /// The value can only be set or modified before the first request is made.
- ///
- public string? ApiKey
- {
- set
- {
- _apiKey = value;
- if (!string.IsNullOrEmpty(_apiKey))
- {
- if (Client.DefaultRequestHeaders.Contains("x-goog-api-key"))
- {
- Client.DefaultRequestHeaders.Remove("x-goog-api-key");
- }
- Client.DefaultRequestHeaders.Add("x-goog-api-key", _apiKey);
- }
- }
- }
-
- ///
- /// Sets the project ID to use for the request.
- ///
- ///
- /// The value can only be set or modified before the first request is made.
- ///
- public string? ProjectId
- {
- set
- {
- _projectId = value;
- if (!string.IsNullOrEmpty(_projectId))
- {
- if (Client.DefaultRequestHeaders.Contains("x-goog-user-project"))
- {
- Client.DefaultRequestHeaders.Remove("x-goog-user-project");
- }
- Client.DefaultRequestHeaders.Add("x-goog-user-project", _projectId);
- }
- }
- }
-
- ///
- /// Returns the region to use for the request.
- ///
- public string Region
- {
- get => _region;
- set => _region = value;
- }
-
- ///
- /// Returns the name of the model.
- ///
- /// Name of the model.
- public string Name => _model;
-
- ///
- /// Sets the access token to use for the request.
- ///
- public string? AccessToken
- {
- set
- {
- _accessToken = value;
- if (value != null)
- Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
- }
- }
-
///
/// You can enable Server Sent Events (SSE) for gemini-1.0-pro
///
@@ -206,30 +98,21 @@ public string? AccessToken
/// Activate JSON Mode (default = no)
///
public bool UseJsonMode { get; set; } = false;
-
+
///
- /// Gets or sets the timespan to wait before the request times out.
+ /// Initializes a new instance of the class.
///
- public TimeSpan Timeout
- {
- get => Client.Timeout;
- set => Client.Timeout = value;
- }
+ public GenerativeModel() : this(logger: null) { }
///
/// Initializes a new instance of the class.
/// The default constructor attempts to read .env file and environment variables.
/// Sets default values, if available.
///
- public GenerativeModel()
+ /// Optional. Logger instance used for logging
+ public GenerativeModel(ILogger? logger = null) : base(logger)
{
- _options = DefaultJsonSerializerOptions();
- GenerativeAIExtensions.ReadDotEnv();
- ApiKey = Environment.GetEnvironmentVariable("GOOGLE_API_KEY");
- AccessToken = Environment.GetEnvironmentVariable("GOOGLE_ACCESS_TOKEN"); // ?? GetAccessTokenFromAdc();
- Model = Environment.GetEnvironmentVariable("GOOGLE_AI_MODEL") ??
- GenerativeAI.Model.Gemini15Pro;
- _region = Environment.GetEnvironmentVariable("GOOGLE_REGION") ?? _region;
+ Logger.LogGenerativeModelInvoking();
var productHeaderValue = new ProductHeaderValue(name: "Mscc.GenerativeAI",
version: Assembly.GetExecutingAssembly().GetName().Version?.ToString());
@@ -247,21 +130,25 @@ public GenerativeModel()
/// Optional. A list of Tools the model may use to generate the next response.
/// Optional.
/// Optional. Configuration of tools.
+ /// Optional. Logger instance used for logging
internal GenerativeModel(string? apiKey = null,
string? model = null,
GenerationConfig? generationConfig = null,
List? safetySettings = null,
List? tools = null,
Content? systemInstruction = null,
- ToolConfig? toolConfig = null) : this()
+ ToolConfig? toolConfig = null,
+ ILogger? logger = null) : this(logger)
{
+ Logger.LogGenerativeModelInvoking();
+
ApiKey = apiKey ?? _apiKey;
Model = model ?? _model;
_generationConfig ??= generationConfig;
_safetySettings ??= safetySettings;
- _tools = tools;
- _toolConfig = toolConfig;
- _systemInstruction = systemInstruction;
+ _tools ??= tools;
+ _toolConfig ??= toolConfig;
+ _systemInstruction ??= systemInstruction;
}
///
@@ -275,29 +162,19 @@ internal GenerativeModel(string? apiKey = null,
/// Optional. A list of Tools the model may use to generate the next response.
/// Optional.
/// Optional. Configuration of tools.
+ /// Optional. Logger instance used for logging
internal GenerativeModel(string? projectId = null, string? region = null,
string? model = null,
GenerationConfig? generationConfig = null,
List? safetySettings = null,
List? tools = null,
Content? systemInstruction = null,
- ToolConfig? toolConfig = null) : this()
+ ToolConfig? toolConfig = null,
+ ILogger? logger = null) : base(projectId, region, model, logger)
{
+ Logger.LogGenerativeModelInvoking();
+
_useVertexAi = true;
- var credentialsFile =
- Environment.GetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS") ??
- Environment.GetEnvironmentVariable("GOOGLE_WEB_CREDENTIALS") ??
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "gcloud",
- "application_default_credentials.json");
- Credentials? credentials = GetCredentialsFromFile(credentialsFile);
- AccessToken = Environment.GetEnvironmentVariable("GOOGLE_ACCESS_TOKEN") ??
- GetAccessTokenFromAdc();
- ProjectId = projectId ??
- Environment.GetEnvironmentVariable("GOOGLE_PROJECT_ID") ??
- credentials?.ProjectId ??
- _projectId;
- _region = region ?? _region;
- Model = model ?? _model;
_generationConfig = generationConfig;
_safetySettings = safetySettings;
_tools = tools;
@@ -311,10 +188,12 @@ internal GenerativeModel(string? projectId = null, string? region = null,
/// Content that has been preprocessed.
/// Optional. Configuration options for model generation and outputs.
/// Optional. A list of unique SafetySetting instances for blocking unsafe content.
+ /// Optional. Logger instance used for logging
/// Thrown when is null.
internal GenerativeModel(CachedContent cachedContent,
GenerationConfig? generationConfig = null,
- List? safetySettings = null) : this()
+ List? safetySettings = null,
+ ILogger? logger = null) : this(logger)
{
_cachedContent = cachedContent ?? throw new ArgumentNullException(nameof(cachedContent));
@@ -328,6 +207,43 @@ internal GenerativeModel(CachedContent cachedContent,
#region Undecided location of methods.Maybe IGenerativeAI might be better...
+ ///
+ /// Get a list of available tuned models and description.
+ ///
+ /// List of available tuned models.
+ /// The maximum number of Models to return (per page).
+ /// A page token, received from a previous ListModels call. Provide the pageToken returned by one request as an argument to the next request to retrieve the next page.
+ /// Optional. A filter is a full text search over the tuned model's description and display name. By default, results will not include tuned models shared with everyone. Additional operators: - owner:me - writers:me - readers:me - readers:everyone
+ ///
+ private async Task> ListTunedModels(int? pageSize = null,
+ string? pageToken = null,
+ string? filter = null)
+ {
+ if (_useVertexAi)
+ {
+ throw new NotSupportedException();
+ }
+
+ if (!string.IsNullOrEmpty(_apiKey))
+ {
+ throw new NotSupportedException("Accessing tuned models via API key is not provided. Setup OAuth for your project.");
+ }
+
+ var url = "{endpointGoogleAI}/{Version}/tunedModels"; // v1beta3
+ var queryStringParams = new Dictionary()
+ {
+ [nameof(pageSize)] = Convert.ToString(pageSize),
+ [nameof(pageToken)] = pageToken,
+ [nameof(filter)] = filter
+ };
+
+ url = ParseUrl(url).AddQueryString(queryStringParams);
+ var response = await Client.GetAsync(url);
+ await response.EnsureSuccessAsync();
+ var models = await Deserialize(response);
+ return models?.TunedModels!;
+ }
+
///
/// Lists models available through the API.
///
@@ -764,7 +680,10 @@ public async Task GenerateContent(GenerateContentReques
request.GenerationConfig.ResponseMimeType = Constants.MediaType;
}
string json = Serialize(request);
- var payload = new StringContent(json, Encoding.UTF8, Constants.MediaType);
+
+ Logger.LogGenerativeModelInvokingRequest(nameof(GenerateContent), url, json);
+
+ var payload = new StringContent(json, Encoding.UTF8, Constants.MediaType);
if (requestOptions != null)
{
@@ -923,6 +842,8 @@ public async IAsyncEnumerable GenerateContentStream(Gen
request.GenerationConfig.ResponseMimeType = Constants.MediaType;
}
+ if (Logger.IsEnabled(LogLevel.Debug)) Logger.LogGenerativeModelInvokingRequest(nameof(GenerateContentStream), url, Serialize(request));
+
// Ref: https://code-maze.com/using-streams-with-httpclient-to-improve-performance-and-memory-usage/
// Ref: https://www.stevejgordon.co.uk/using-httpcompletionoption-responseheadersread-to-improve-httpclient-performance-dotnet
var ms = new MemoryStream();
@@ -1577,244 +1498,5 @@ public async Task BatchEmbedText(BatchEmbedTextRequest reques
}
#endregion
-
- #region "Private methods"
-
- ///
- /// Get a list of available tuned models and description.
- ///
- /// List of available tuned models.
- /// The maximum number of Models to return (per page).
- /// A page token, received from a previous ListModels call. Provide the pageToken returned by one request as an argument to the next request to retrieve the next page.
- /// Optional. A filter is a full text search over the tuned model's description and display name. By default, results will not include tuned models shared with everyone. Additional operators: - owner:me - writers:me - readers:me - readers:everyone
- ///
- private async Task> ListTunedModels(int? pageSize = null,
- string? pageToken = null,
- string? filter = null)
- {
- if (_useVertexAi)
- {
- throw new NotSupportedException();
- }
-
- if (!string.IsNullOrEmpty(_apiKey))
- {
- throw new NotSupportedException("Accessing tuned models via API key is not provided. Setup OAuth for your project.");
- }
-
- var url = "{endpointGoogleAI}/{Version}/tunedModels"; // v1beta3
- var queryStringParams = new Dictionary()
- {
- [nameof(pageSize)] = Convert.ToString(pageSize),
- [nameof(pageToken)] = pageToken,
- [nameof(filter)] = filter
- };
-
- url = ParseUrl(url).AddQueryString(queryStringParams);
- var response = await Client.GetAsync(url);
- await response.EnsureSuccessAsync();
- var models = await Deserialize(response);
- return models?.TunedModels!;
- }
-
- ///
- /// Parses the URL template and replaces the placeholder with current values.
- /// Given two API endpoints for Google AI Gemini and Vertex AI Gemini this
- /// method uses regular expressions to replace placeholders in a URL template with actual values.
- ///
- /// API endpoint to parse.
- /// Method part of the URL to inject
- ///
- private string ParseUrl(string url, string method = "")
- {
- var replacements = GetReplacements();
- replacements.Add("method", method);
-
- var urlParsed = Regex.Replace(url, @"\{(?.*?)\}",
- match => replacements.TryGetValue(match.Groups["name"].Value, out var value) ? value : "");
-
- return urlParsed;
-
- Dictionary GetReplacements()
- {
- return new Dictionary(StringComparer.OrdinalIgnoreCase)
- {
- { "endpointGoogleAI", EndpointGoogleAi },
- { "version", Version },
- { "model", _model },
- { "apikey", _apiKey ?? "" },
- { "projectid", _projectId ?? "" },
- { "region", _region },
- { "location", _region },
- { "publisher", _publisher }
- };
- }
- }
-
- ///
- /// Return serialized JSON string of request payload.
- ///
- ///
- ///
- private string Serialize(T request)
- {
- return JsonSerializer.Serialize(request, _options);
- }
-
- ///
- /// Return deserialized object from JSON response.
- ///
- ///
- ///
- ///
- private async Task Deserialize(HttpResponseMessage response)
- {
-#if NET472_OR_GREATER || NETSTANDARD2_0
- var json = await response.Content.ReadAsStringAsync();
- return JsonSerializer.Deserialize(json, _options);
-#else
- var json = await response.Content.ReadAsStringAsync();
- return await response.Content.ReadFromJsonAsync(_options);
-#endif
- }
-
- ///
- /// Get default options for JSON serialization.
- ///
- ///
- internal JsonSerializerOptions DefaultJsonSerializerOptions()
- {
- var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
- {
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
- NumberHandling = JsonNumberHandling.AllowReadingFromString,
- PropertyNameCaseInsensitive = true,
- ReadCommentHandling = JsonCommentHandling.Skip,
- AllowTrailingCommas = true,
- //WriteIndented = true,
- };
- options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper));
-
- 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;
- }
-
- ///
- /// This method uses the gcloud command-line tool to retrieve an access token from the Application Default Credentials (ADC).
- /// It is specific to Google Cloud Platform and allows easy authentication with the Gemini API on Google Cloud.
- /// Reference: https://cloud.google.com/docs/authentication
- ///
- /// The access token.
- private string GetAccessTokenFromAdc()
- {
- if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
- {
- return RunExternalExe("cmd.exe", "/c gcloud auth application-default print-access-token").TrimEnd();
- }
- else
- {
- return RunExternalExe("gcloud", "auth application-default print-access-token").TrimEnd();
- }
- }
-
- ///
- /// Run an external application as process in the underlying operating system, if possible.
- ///
- /// The command or application to run.
- /// Optional arguments given to the application to run.
- /// Output from the application.
- ///
- private string RunExternalExe(string filename, string arguments)
- {
- var process = new Process();
- var stdOutput = new StringBuilder();
- var stdError = new StringBuilder();
-
- process.StartInfo.FileName = filename;
- if (!string.IsNullOrEmpty(arguments))
- {
- process.StartInfo.Arguments = arguments;
- }
-
- process.StartInfo.CreateNoWindow = true;
- process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
- process.StartInfo.UseShellExecute = false;
-
- process.StartInfo.RedirectStandardError = true;
- process.StartInfo.RedirectStandardOutput = true;
- // Use AppendLine rather than Append since args.Data is one line of output, not including the newline character.
- process.OutputDataReceived += (sender, args) => stdOutput.AppendLine(args.Data);
- process.ErrorDataReceived += (sender, args) => stdError.AppendLine(args.Data);
-
- try
- {
- process.Start();
- process.BeginOutputReadLine();
- process.WaitForExit();
- }
- catch (Exception e)
- {
- // throw new Exception("OS error while executing " + Format(filename, arguments)+ ": " + e.Message, e);
- return string.Empty;
- }
-
- if (process.ExitCode == 0)
- {
- return stdOutput.ToString();
- }
- else
- {
- var message = new StringBuilder();
-
- if (stdError.Length > 0)
- {
- message.AppendLine("Err output:");
- message.AppendLine(stdError.ToString());
- }
-
- if (stdOutput.Length != 0)
- {
- message.AppendLine("Std output:");
- message.AppendLine(stdOutput.ToString());
- }
-
- throw new Exception(Format(filename, arguments) + " finished with exit code = " + process.ExitCode + ": " + message);
- }
- }
-
- ///
- /// Formatting string for logging purpose.
- ///
- /// The command or application to run.
- /// Optional arguments given to the application to run.
- /// Formatted string containing parameter values.
- private string Format(string filename, string? arguments)
- {
- return "'" + filename +
- ((string.IsNullOrEmpty(arguments)) ? string.Empty : " " + arguments) +
- "'";
- }
-
- #endregion
}
}
diff --git a/src/Mscc.GenerativeAI/GoogleAI.cs b/src/Mscc.GenerativeAI/GoogleAI.cs
index 3cb0f3e..2bc1550 100644
--- a/src/Mscc.GenerativeAI/GoogleAI.cs
+++ b/src/Mscc.GenerativeAI/GoogleAI.cs
@@ -6,6 +6,7 @@
using System.Threading;
using System.Threading.Tasks;
#endif
+using Microsoft.Extensions.Logging;
namespace Mscc.GenerativeAI
{
@@ -15,7 +16,7 @@ namespace Mscc.GenerativeAI
///
/// See Model reference.
///
- public sealed class GoogleAI : IGenerativeAI
+ public sealed class GoogleAI : BaseLogger, IGenerativeAI
{
private readonly string? _apiKey;
private readonly string? _accessToken;
@@ -36,7 +37,7 @@ public sealed class GoogleAI : IGenerativeAI
/// Optional. Access token provided by OAuth 2.0 or Application Default Credentials (ADC).
///
///
- private GoogleAI()
+ private GoogleAI(ILogger? logger = null) : base(logger)
{
GenerativeAIExtensions.ReadDotEnv();
_apiKey = Environment.GetEnvironmentVariable("GOOGLE_API_KEY");
@@ -49,7 +50,8 @@ private GoogleAI()
///
/// Identifier of the Google Cloud project
/// Access token for the Google Cloud project
- public GoogleAI(string? apiKey = null, string? accessToken = null) : this()
+ /// Optional. Logger instance used for logging
+ public GoogleAI(string? apiKey = null, string? accessToken = null, ILogger? logger = null) : this(logger)
{
_apiKey ??= apiKey;
_accessToken ??= accessToken;
diff --git a/src/Mscc.GenerativeAI/ImageGenerationModel.cs b/src/Mscc.GenerativeAI/ImageGenerationModel.cs
index feeefa6..d92e2c1 100644
--- a/src/Mscc.GenerativeAI/ImageGenerationModel.cs
+++ b/src/Mscc.GenerativeAI/ImageGenerationModel.cs
@@ -4,6 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
#endif
+using Microsoft.Extensions.Logging;
using System.Text;
namespace Mscc.GenerativeAI
@@ -12,7 +13,7 @@ namespace Mscc.GenerativeAI
/// Name of the model that supports image generation.
/// The can create high quality visual assets in seconds and brings Google's state-of-the-art vision and multimodal generative AI capabilities to application developers.
///
- public class ImageGenerationModel : BaseGeneration
+ public sealed class ImageGenerationModel : BaseModel
{
private const string UrlVertexAi =
"https://{region}-aiplatform.googleapis.com/{version}/projects/{projectId}/locations/{region}/publishers/{publisher}/models/{model}:{method}";
@@ -20,13 +21,18 @@ public class ImageGenerationModel : BaseGeneration
private string Url => UrlVertexAi;
private string Method => GenerativeAI.Method.Predict;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ImageGenerationModel() : this(logger: null) { }
///
/// Initializes a new instance of the class.
/// The default constructor attempts to read .env file and environment variables.
/// Sets default values, if available.
///
- public ImageGenerationModel() { }
+ public ImageGenerationModel(ILogger? logger = null) : base(logger) { }
///
/// Initializes a new instance of the class with access to Vertex AI Gemini API.
@@ -34,10 +40,9 @@ public ImageGenerationModel() { }
/// Identifier of the Google Cloud project
/// Region to use
/// Model to use
+ /// Optional. Logger instance used for logging
public ImageGenerationModel(string? projectId = null, string? region = null,
- string? model = null) : base(projectId, region, model)
- {
- }
+ string? model = null, ILogger? logger = null) : base(projectId, region, model, logger) { }
///
/// Generates images from the specified .
diff --git a/src/Mscc.GenerativeAI/ImageTextModel.cs b/src/Mscc.GenerativeAI/ImageTextModel.cs
index 8438874..0d2b7bb 100644
--- a/src/Mscc.GenerativeAI/ImageTextModel.cs
+++ b/src/Mscc.GenerativeAI/ImageTextModel.cs
@@ -4,6 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
#endif
+using Microsoft.Extensions.Logging;
using System.Text;
namespace Mscc.GenerativeAI
@@ -12,7 +13,7 @@ namespace Mscc.GenerativeAI
/// Name of the model that supports image captioning.
/// generates a caption from an image you provide based on the language that you specify. The model supports the following languages: English (en), German (de), French (fr), Spanish (es) and Italian (it).
///
- public class ImageTextModel : BaseGeneration
+ public sealed class ImageTextModel : BaseModel
{
private const string UrlVertexAi =
"https://{region}-aiplatform.googleapis.com/{version}/projects/{projectId}/locations/{region}/publishers/{publisher}/models/{model}:{method}";
@@ -20,13 +21,18 @@ public class ImageTextModel : BaseGeneration
private string Url => UrlVertexAi;
private string Method => GenerativeAI.Method.Predict;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ImageTextModel() : this(logger: null) { }
///
/// Initializes a new instance of the class.
/// The default constructor attempts to read .env file and environment variables.
/// Sets default values, if available.
///
- public ImageTextModel() { }
+ public ImageTextModel(ILogger? logger = null) : base(logger) { }
///
/// Initializes a new instance of the class with access to Vertex AI Gemini API.
@@ -34,10 +40,9 @@ public ImageTextModel() { }
/// Identifier of the Google Cloud project
/// Region to use
/// Model to use
+ /// Optional. Logger instance used for logging
public ImageTextModel(string? projectId = null, string? region = null,
- string? model = null) : base(projectId, region, model)
- {
- }
+ string? model = null, ILogger? logger = null) : base(projectId, region, model, logger) { }
///
/// Generates images from the specified .
diff --git a/src/Mscc.GenerativeAI/Logging/GenerativeModelLogMessages.cs b/src/Mscc.GenerativeAI/Logging/GenerativeModelLogMessages.cs
new file mode 100644
index 0000000..c67c285
--- /dev/null
+++ b/src/Mscc.GenerativeAI/Logging/GenerativeModelLogMessages.cs
@@ -0,0 +1,58 @@
+using Microsoft.Extensions.Logging;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Mscc.GenerativeAI
+{
+ #pragma warning disable SYSLIB1006 // Multiple logging methods cannot use the same event id within a class
+
+ ///
+ /// Extensions for logging invocations.
+ ///
+ ///
+ /// This extension uses the to
+ /// generate logging code at compile time to achieve optimized code.
+ ///
+ [ExcludeFromCodeCoverage]
+ internal static partial class GenerativeModelLogMessages
+ {
+ ///
+ /// Logs
+ ///
+ /// Optional. Logger instance used for logging
+ [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = "Generative model starting")]
+ public static partial void LogGenerativeModelInvoking(
+ this ILogger logger);
+
+ ///
+ /// Logs
+ ///
+ /// Optional. Logger instance used for logging
+ [LoggerMessage(EventId = 0, Level = LogLevel.Information, Message = "Generative model started")]
+ public static partial void LogGenerativeModelInvoked(
+ this ILogger logger);
+
+ ///
+ /// Logs invoking an API request.
+ ///
+ /// Optional. Logger instance used for logging
+ /// Calling method
+ /// URL of Gemini API endpoint
+ /// Data sent to the API endpoint
+ [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = "[{MethodName}] Request: {Url} - {Payload}")]
+ public static partial void LogGenerativeModelInvokingRequest(
+ this ILogger logger,
+ string methodName,
+ string url,
+ string payload);
+
+ ///
+ /// Logs when exception thrown to run an external application.
+ ///
+ /// Optional. Logger instance used for logging
+ /// Message of to log.
+ [LoggerMessage(EventId = 0, Level = LogLevel.Warning, Message = "{Message}")]
+ public static partial void LogRunExternalExe(
+ this ILogger logger,
+ string message);
+ }
+}
\ No newline at end of file
diff --git a/src/Mscc.GenerativeAI/MediaModel.cs b/src/Mscc.GenerativeAI/MediaModel.cs
index 955d4c8..fd80abe 100644
--- a/src/Mscc.GenerativeAI/MediaModel.cs
+++ b/src/Mscc.GenerativeAI/MediaModel.cs
@@ -2,24 +2,31 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Net.Http;
-using System.Security.Authentication;
-using System.Text.Json;
-using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
#endif
+using Microsoft.Extensions.Logging;
using System.Net.Http.Headers;
using System.Text;
namespace Mscc.GenerativeAI
{
- public class MediaModel : BaseGeneration
+ public sealed class MediaModel : BaseModel
{
protected override string Version => ApiVersion.V1Beta;
- private const string EndpointGoogleAi = "https://generativelanguage.googleapis.com";
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MediaModel() : this(logger: null) { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Optional. Logger instance used for logging
+ public MediaModel(ILogger? logger) : base(logger) { }
+
///
/// Uploads a file to the File API backend.
///
@@ -68,21 +75,20 @@ public async Task UploadFile(string uri,
});
string json = Serialize(request);
- using (var fs = new FileStream(uri, FileMode.Open)){
- var multipartContent = new MultipartContent("related");
- multipartContent.Add(new StringContent(json, Encoding.UTF8, Constants.MediaType));
- multipartContent.Add(new StreamContent(fs, (int)Constants.ChunkSize)
- {
- Headers = {
- ContentType = new MediaTypeHeaderValue(mimeType),
- ContentLength = totalBytes
- }
- });
+ using var fs = new FileStream(uri, FileMode.Open);
+ var multipartContent = new MultipartContent("related");
+ multipartContent.Add(new StringContent(json, Encoding.UTF8, Constants.MediaType));
+ multipartContent.Add(new StreamContent(fs, (int)Constants.ChunkSize)
+ {
+ Headers = {
+ ContentType = new MediaTypeHeaderValue(mimeType),
+ ContentLength = totalBytes
+ }
+ });
- var response = await Client.PostAsync(url, multipartContent, cancellationToken);
- await response.EnsureSuccessAsync();
- return await Deserialize(response);
- }
+ var response = await Client.PostAsync(url, multipartContent, cancellationToken);
+ await response.EnsureSuccessAsync();
+ return await Deserialize(response);
}
///
diff --git a/src/Mscc.GenerativeAI/Mscc.GenerativeAI.csproj b/src/Mscc.GenerativeAI/Mscc.GenerativeAI.csproj
index ec1a05a..4c0d2ee 100644
--- a/src/Mscc.GenerativeAI/Mscc.GenerativeAI.csproj
+++ b/src/Mscc.GenerativeAI/Mscc.GenerativeAI.csproj
@@ -67,6 +67,7 @@
+
diff --git a/src/Mscc.GenerativeAI/Types/BaseLogger.cs b/src/Mscc.GenerativeAI/Types/BaseLogger.cs
new file mode 100644
index 0000000..a349ed2
--- /dev/null
+++ b/src/Mscc.GenerativeAI/Types/BaseLogger.cs
@@ -0,0 +1,19 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Mscc.GenerativeAI
+{
+ ///
+ /// Abstract base type with logging instance.
+ ///
+ public abstract class BaseLogger
+ {
+ protected ILogger Logger { get; }
+
+ ///
+ /// Base constructor to set the instance.
+ ///
+ /// Optional. Logger instance used for logging
+ protected BaseLogger(ILogger? logger) => Logger = logger ?? NullLogger.Instance;
+ }
+}
\ No newline at end of file
diff --git a/src/Mscc.GenerativeAI/Types/Generative/Credentials.cs b/src/Mscc.GenerativeAI/Types/Generative/Credentials.cs
index eace5db..e7241f4 100644
--- a/src/Mscc.GenerativeAI/Types/Generative/Credentials.cs
+++ b/src/Mscc.GenerativeAI/Types/Generative/Credentials.cs
@@ -5,7 +5,7 @@ namespace Mscc.GenerativeAI
/// It de/serializes the content of the client_secret.json file for OAuth 2.0
/// using either Desktop or Web approach, and supports Service Accounts on Google Cloud Platform.
///
- internal class Credentials : ClientSecrets
+ public sealed class Credentials : ClientSecrets
{
private string _projectId;
@@ -47,7 +47,7 @@ public string ProjectId
///
/// Project ID (quota) in Google Cloud Platform.
///
- public virtual string QuotaProjectId
+ public string QuotaProjectId
{
get => _projectId;
set => _projectId = value;
@@ -58,7 +58,7 @@ public virtual string QuotaProjectId
/// Represents the content of a client_secret.json file used in Google Cloud Platform
/// to authenticate a user or service account.
///
- internal class ClientSecrets
+ public class ClientSecrets
{
///
/// Client ID
diff --git a/src/Mscc.GenerativeAI/VertexAI.cs b/src/Mscc.GenerativeAI/VertexAI.cs
index 8340f61..4961346 100644
--- a/src/Mscc.GenerativeAI/VertexAI.cs
+++ b/src/Mscc.GenerativeAI/VertexAI.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
#endif
+using Microsoft.Extensions.Logging;
namespace Mscc.GenerativeAI
{
@@ -13,7 +14,7 @@ namespace Mscc.GenerativeAI
/// See Model reference.
/// See also https://cloud.google.com/nodejs/docs/reference/vertexai/latest/vertexai/vertexinit
///
- public sealed class VertexAI : IGenerativeAI
+ public sealed class VertexAI : BaseLogger, IGenerativeAI
{
private readonly string? _projectId;
private readonly string _region = "us-central1";
@@ -36,7 +37,7 @@ public sealed class VertexAI : IGenerativeAI
/// Identifier of the Google Cloud region to use (default: "us-central1").
///
///
- private VertexAI()
+ private VertexAI(ILogger? logger = null) : base(logger)
{
GenerativeAIExtensions.ReadDotEnv();
_projectId = Environment.GetEnvironmentVariable("GOOGLE_PROJECT_ID");
@@ -47,9 +48,10 @@ private VertexAI()
/// Initializes a new instance of the class with access to Vertex AI Gemini API.
///
/// Identifier of the Google Cloud project.
- /// Region to use (default: "us-central1").
+ /// Optional. Region to use (default: "us-central1").
+ /// Optional. Logger instance used for logging
/// Thrown when is .
- public VertexAI(string? projectId, string? region = null) : this()
+ public VertexAI(string? projectId, string? region = null, ILogger? logger = null) : this(logger)
{
_projectId ??= projectId ?? throw new ArgumentNullException(nameof(projectId));
_region = region ?? _region;