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;