diff --git a/README.md b/README.md index 4c4ee6c6..e43562cd 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,38 @@ Use `git log -1` to review the last commit details and find the automatically ge --- +### Model Configuration and Settings + +To configure and use models with `dotnet-aicommitmessage`, users need to set their settings once. This setup involves specifying the model, API key, and API URL. These settings will be stored as environment variables for future use. + +#### Initial Setup + +Run the following commands to configure the model and related settings: + +``` +dotnet-aicommitmessage set-settings -m gpt-4o-mini -k {api-key} -u {api-url} +dotnet-aicommitmessage set-settings -m llama-3-1-405b-instruct -k {api-key} -u {api-url} +``` + +Replace `{api-key}` with your API key and `{api-url}` with the URL of your API provider. + +#### Switching Models + +After the initial setup, you can easily switch between models without needing to provide the API key or URL again: + +``` +dotnet-aicommitmessage set-settings -m gpt-4o-mini +dotnet-aicommitmessage set-settings -m llama-3-1-405b-instruct +``` + +This allows for quick model changes while retaining your previously configured API details. + +#### Supported Models + +Currently supported models are `gpt-4o-mini` and `llama-3-1-405b-instruct`. + +--- + ## Commit message pattern The training model for the AI used is designed using as reference these guidelines: diff --git a/Src/AiCommitMessage/AiCommitMessage.csproj b/Src/AiCommitMessage/AiCommitMessage.csproj index 2da99183..7557acc8 100644 --- a/Src/AiCommitMessage/AiCommitMessage.csproj +++ b/Src/AiCommitMessage/AiCommitMessage.csproj @@ -28,6 +28,7 @@ + diff --git a/Src/AiCommitMessage/Options/SetSettingsOptions.cs b/Src/AiCommitMessage/Options/SetSettingsOptions.cs index 0934bf52..1a757ed6 100644 --- a/Src/AiCommitMessage/Options/SetSettingsOptions.cs +++ b/Src/AiCommitMessage/Options/SetSettingsOptions.cs @@ -5,28 +5,28 @@ namespace AiCommitMessage.Options; /// /// Class SetSettingsOptions. /// -[Verb("set-settings", HelpText = "Set the OpenAI settings.")] +[Verb("set-settings", HelpText = "Set the AI model settings.")] public class SetSettingsOptions { /// /// Gets or sets the URL. /// /// The URL. - [Option('u', "url", Required = false, HelpText = "The OpenAI url.")] + [Option('u', "url", Required = false, HelpText = "The model url.")] public string Url { get; set; } /// /// Gets or sets the key. /// /// The key. - [Option('k', "key", Required = false, HelpText = "The OpenAI API key.")] + [Option('k', "key", Required = false, HelpText = "The model API key.")] public string Key { get; set; } /// /// Gets or sets the model. /// /// The model. - [Option('m', "model", Required = false, HelpText = "The OpenAI model.")] + [Option('m', "model", Required = false, HelpText = "The model name (e.g., GPT-4o, Llama-3-1-405B-Instruct).")] public string Model { get; set; } /// @@ -40,6 +40,6 @@ public class SetSettingsOptions /// Gets or sets a value indicating whether [save encrypted]. /// /// true if [save encrypted]; otherwise, false. - [Option('e', "encrypted", Required = false, HelpText = "Persiste key encrypted or plain-text.")] + [Option('e', "encrypted", Required = false, HelpText = "Persist key encrypted or plain-text.")] public bool SaveEncrypted { get; set; } } diff --git a/Src/AiCommitMessage/Services/GenerateCommitMessageService.cs b/Src/AiCommitMessage/Services/GenerateCommitMessageService.cs index 98fe2c3b..f77da9a9 100644 --- a/Src/AiCommitMessage/Services/GenerateCommitMessageService.cs +++ b/Src/AiCommitMessage/Services/GenerateCommitMessageService.cs @@ -4,6 +4,8 @@ using System.Text.RegularExpressions; using AiCommitMessage.Options; using AiCommitMessage.Utility; +using Azure; +using Azure.AI.Inference; using OpenAI; using OpenAI.Chat; @@ -37,10 +39,10 @@ private static bool IsMergeConflictResolution(string message) => /// Generates a commit message based on the provided options and the OpenAI API. /// /// An instance of containing the branch name, original message, and git diff. - /// A string containing the generated commit message from the OpenAI API. + /// A string containing the generated commit message from the API. /// /// This method retrieves API details (URL and key) from environment variables, constructs a message including the branch name, - /// original commit message, and git diff, and sends it to the OpenAI API for processing. It also handles debugging, saving + /// original commit message, and git diff, and sends it to the respective API for processing. It also handles debugging, saving /// API responses to a JSON file if debugging is enabled. If the commit message is a merge conflict resolution, it is returned as-is. /// /// Thrown if both the branch and diff are empty, as meaningful commit generation is not possible. @@ -74,23 +76,74 @@ public string GenerateCommitMessage(GenerateCommitMessageOptions options) + "Git Diff: " + (string.IsNullOrEmpty(diff) ? "" : diff); - var model = EnvironmentLoader.LoadOpenAiModel(); - var url = EnvironmentLoader.LoadOpenAiApiUrl(); - var key = EnvironmentLoader.LoadOpenAiApiKey(); + var model = EnvironmentLoader.LoadModelName(); + return GenerateWithModel(model, formattedMessage, branch, message, options.Debug); + } + + private static string GenerateWithModel(string model, string formattedMessage, string branch, string message, bool debug) + { + string text; + + if (model.Equals("llama-3-1-405B-Instruct", StringComparison.OrdinalIgnoreCase)) + { + var endpoint = new Uri(EnvironmentLoader.LoadLlamaApiUrl()); + var credential = new AzureKeyCredential(EnvironmentLoader.LoadLlamaApiKey()); + + var client = new ChatCompletionsClient(endpoint, credential, new AzureAIInferenceClientOptions()); + + var requestOptions = new ChatCompletionsOptions + { + Messages = + { + new ChatRequestSystemMessage(Constants.SystemMessage), + new ChatRequestUserMessage(formattedMessage), + }, + Temperature = 1.0f, + NucleusSamplingFactor = 1.0f, + MaxTokens = 1000, + Model = "Meta-Llama-3.1-405B-Instruct" + }; + + var response = client.Complete(requestOptions); + text = response.Value.Content; + } + else if (model.Equals("gpt-4o-mini", StringComparison.OrdinalIgnoreCase)) + { + var apiUrl = EnvironmentLoader.LoadOpenAiApiUrl(); + var apiKey = EnvironmentLoader.LoadOpenAiApiKey(); + + var client = new ChatClient( + "gpt-4o-mini", + new ApiKeyCredential(apiKey), + new OpenAIClientOptions { Endpoint = new Uri(apiUrl) } + ); + + var chatCompletion = client.CompleteChat( + new SystemChatMessage(Constants.SystemMessage), + new UserChatMessage(formattedMessage) + ); + + text = chatCompletion.Value.Content[0].Text; + } + else + { + throw new NotSupportedException($"Model '{model}' is not supported."); + } - var client = new ChatClient( - model, - new ApiKeyCredential(key), - new OpenAIClientOptions { Endpoint = new Uri(url) } - ); + text = ProcessGeneratedMessage(text, branch, message); - var chatCompletion = client.CompleteChat( - new SystemChatMessage(Constants.SystemMessage), - new UserChatMessage(formattedMessage) - ); + if (!debug) + { + return text; + } - var text = chatCompletion.Value.Content[0].Text; + SaveDebugInfo(text); + return text; + } + + private static string ProcessGeneratedMessage(string text, string branch, string message) + { if (text.Length >= 7 && text[..7] == "type - ") { text = text[7..]; @@ -120,15 +173,13 @@ public string GenerateCommitMessage(GenerateCommitMessageOptions options) text = $"{text} {gitVersionCommand}"; } - if (!options.Debug) - { - return text; - } + return text; + } - var json = JsonSerializer.Serialize(chatCompletion); + private static void SaveDebugInfo(string text) + { + var json = JsonSerializer.Serialize(new { DebugInfo = text }); File.WriteAllText("debug.json", json); - - return text; } /// diff --git a/Src/AiCommitMessage/Services/SettingsService.cs b/Src/AiCommitMessage/Services/SettingsService.cs index 3cb84888..25bc4f12 100644 --- a/Src/AiCommitMessage/Services/SettingsService.cs +++ b/Src/AiCommitMessage/Services/SettingsService.cs @@ -1,4 +1,6 @@ using AiCommitMessage.Options; +using AiCommitMessage.Utility; +using Spectre.Console; namespace AiCommitMessage.Services; @@ -17,21 +19,51 @@ public static class SettingsService /// public static void SetSettings(SetSettingsOptions setSettingsOptions) { - Environment.SetEnvironmentVariable( - "OPENAI_API_KEY", - setSettingsOptions.Key, - EnvironmentVariableTarget.User - ); + if (!string.IsNullOrWhiteSpace(setSettingsOptions.Model)) + { + Environment.SetEnvironmentVariable( + "AI_MODEL", + setSettingsOptions.Model, + EnvironmentVariableTarget.User + ); + } + + var model = EnvironmentLoader.LoadModelName(); + + if (model.Equals("gpt-4o-mini", StringComparison.OrdinalIgnoreCase)) + { + EnvironmentLoader.SetEnvironmentVariableIfProvided( + "OPENAI_API_KEY", + setSettingsOptions.Key, + EnvironmentLoader.LoadOpenAiApiKey() + ); - if (string.IsNullOrWhiteSpace(setSettingsOptions.Url)) + EnvironmentLoader.SetEnvironmentVariableIfProvided( + "OPENAI_API_URL", + setSettingsOptions.Url, + EnvironmentLoader.LoadOpenAiApiUrl() + ); + } + else if (model.Equals("llama-3-1-405B-Instruct", StringComparison.OrdinalIgnoreCase)) + { + EnvironmentLoader.SetEnvironmentVariableIfProvided( + "LLAMA_API_KEY", + setSettingsOptions.Key, + EnvironmentLoader.LoadLlamaApiKey() + ); + + EnvironmentLoader.SetEnvironmentVariableIfProvided( + "LLAMA_API_URL", + setSettingsOptions.Url, + EnvironmentLoader.LoadLlamaApiUrl() + ); + } + else { + AnsiConsole.MarkupLine($"[red]{model} is not supported.[/]"); return; } - Environment.SetEnvironmentVariable( - "OPEN_API_URL", - setSettingsOptions.Url, - EnvironmentVariableTarget.User - ); + AnsiConsole.MarkupLine($"[green]Successfully switched to {model}[/]"); } } diff --git a/Src/AiCommitMessage/Utility/EnvironmentLoader.cs b/Src/AiCommitMessage/Utility/EnvironmentLoader.cs index 2a3674e3..6dbe19e8 100644 --- a/Src/AiCommitMessage/Utility/EnvironmentLoader.cs +++ b/Src/AiCommitMessage/Utility/EnvironmentLoader.cs @@ -17,7 +17,7 @@ public static class EnvironmentLoader /// This allows for flexibility in specifying which model to use without hardcoding it into the application. /// It is particularly useful in scenarios where different models may be used in different environments, such as development, testing, or production. /// - public static string LoadOpenAiModel() => GetEnvironmentVariable("OPENAI_MODEL", "gpt-4o-mini"); + public static string LoadModelName() => GetEnvironmentVariable("AI_MODEL", "gpt-4o-mini"); /// /// Loads the OpenAI API URL from the environment variables. @@ -33,10 +33,10 @@ public static string LoadOpenAiApiUrl() => GetEnvironmentVariable("OPENAI_API_URL", "https://api.openai.com/v1"); /// - /// Loads the OpenAI API key. + /// Loads the OpenAI API key from the environment variables. /// - /// System.String. - /// Please set the OPENAI_API_KEY environment variable. + /// A string representing the OpenAI API key. + /// Thrown if the API key is not set in the environment variables. public static string LoadOpenAiApiKey() { var encryptStr = GetEnvironmentVariable("OPENAI_KEY_ENCRYPTED", "false"); @@ -55,7 +55,21 @@ public static string LoadOpenAiApiKey() } /// - /// Loads the optional emoji. + /// Loads the Llama API key from the environment variables. + /// + /// A string representing the Llama API key. + public static string LoadLlamaApiKey() => + GetEnvironmentVariable("LLAMA_API_KEY", string.Empty); + + /// + /// Loads the Llama API URL from the environment variables. + /// + /// A string representing the Llama API URL. + public static string LoadLlamaApiUrl() => + GetEnvironmentVariable("LLAMA_API_URL", string.Empty); + + /// + /// Loads the optional emoji setting from the environment variables. /// /// true if should include emoji in the commit message, false otherwise. public static bool LoadOptionalEmoji() => @@ -64,8 +78,8 @@ public static bool LoadOptionalEmoji() => /// /// Decrypts the specified encrypted text. /// - /// The encrypted text. - /// System.String. + /// The encrypted text to decrypt. + /// A string representing the decrypted text. private static string Decrypt(string encryptedText) { // Placeholder for decryption logic @@ -96,4 +110,22 @@ private static string GetEnvironmentVariable(string name, string defaultValue) return !string.IsNullOrWhiteSpace(value) ? value : defaultValue; } + + public static void SetEnvironmentVariableIfProvided( + string variableName, + string newValue, + string existingValue + ) + { + if (!string.IsNullOrWhiteSpace(newValue)) + { + Environment.SetEnvironmentVariable(variableName, newValue, EnvironmentVariableTarget.User); + Environment.SetEnvironmentVariable(variableName, newValue, EnvironmentVariableTarget.Process); + } + else if (!string.IsNullOrWhiteSpace(existingValue)) + { + Environment.SetEnvironmentVariable(variableName, existingValue, EnvironmentVariableTarget.User); + Environment.SetEnvironmentVariable(variableName, existingValue, EnvironmentVariableTarget.Process); + } + } } diff --git a/Tests/AiCommitMessage.Tests/Services/GenerateCommitMessageServiceTests.cs b/Tests/AiCommitMessage.Tests/Services/GenerateCommitMessageServiceTests.cs index 3f97f98f..be8a3580 100644 --- a/Tests/AiCommitMessage.Tests/Services/GenerateCommitMessageServiceTests.cs +++ b/Tests/AiCommitMessage.Tests/Services/GenerateCommitMessageServiceTests.cs @@ -1,5 +1,7 @@ +using System.Text.RegularExpressions; using AiCommitMessage.Options; using AiCommitMessage.Services; +using AiCommitMessage.Utility; using FluentAssertions; namespace AiCommitMessage.Tests.Services; @@ -118,4 +120,43 @@ public void GenerateCommitMessage_Should_ReturnMessage_When_MergeConflictResolut // var debugFileContent = File.ReadAllText("debug.json"); // debugFileContent.Should().Be(JsonSerializer.Serialize(chatCompletionResult)); //} + + [Fact] + public void GenerateCommitMessage_WithLlamaModel_Should_MatchExpectedPattern() + { + // Arrange + Environment.SetEnvironmentVariable("AI_MODEL", "llama-3-1-405B-Instruct"); + var options = new GenerateCommitMessageOptions + { + Branch = "feature/llama", + Diff = "Add llama-specific functionality", + Message = "Initial llama commit" + }; + + // Act + var result = _service.GenerateCommitMessage(options); + + // Assert + result.Should().MatchRegex("(?i)(?=.*add)(?=.*llama)"); + } + [Fact] + public void GenerateCommitMessage_WithGPTModel_Should_MatchExpectedPattern() + { + // Arrange + Environment.SetEnvironmentVariable("AI_MODEL", "gpt-4o-mini", EnvironmentVariableTarget.User); + + var service = new GenerateCommitMessageService(); + var options = new GenerateCommitMessageOptions + { + Branch = "feature/gpt", + Diff = "Add GPT-specific improvements", + Message = "Initial GPT commit" + }; + + // Act + var result = service.GenerateCommitMessage(options); + + // Assert + result.Should().MatchRegex("(?i)(?=.*add)(?=.*gpt)"); + } }