From 1044915be100ea75e2a7b475cd67ddabd14d0174 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 28 Oct 2024 11:00:11 +0800 Subject: [PATCH] refactor: OpenAI integration * supports configure multiple services * supports select service when generate commit message by OpenAI Signed-off-by: leo --- src/Commands/GenerateCommitMessage.cs | 8 +- src/Models/OpenAI.cs | 81 ++++++++--- src/Resources/Locales/en_US.axaml | 2 +- src/Resources/Locales/ru_RU.axaml | 1 - src/Resources/Locales/zh_CN.axaml | 8 +- src/Resources/Locales/zh_TW.axaml | 2 +- src/ViewModels/Preference.cs | 113 +--------------- src/ViewModels/WorkingCopy.cs | 64 ++++++--- src/Views/AIAssistant.axaml.cs | 7 +- src/Views/Preference.axaml | 185 +++++++++++++++----------- src/Views/Preference.axaml.cs | 28 ++++ src/Views/WorkingCopy.axaml | 2 +- src/Views/WorkingCopy.axaml.cs | 11 ++ 13 files changed, 283 insertions(+), 229 deletions(-) diff --git a/src/Commands/GenerateCommitMessage.cs b/src/Commands/GenerateCommitMessage.cs index 3bcf7010..e4f25f38 100644 --- a/src/Commands/GenerateCommitMessage.cs +++ b/src/Commands/GenerateCommitMessage.cs @@ -20,8 +20,9 @@ namespace SourceGit.Commands } } - public GenerateCommitMessage(string repo, List changes, CancellationToken cancelToken, Action onProgress) + public GenerateCommitMessage(Models.OpenAIService service, string repo, List changes, CancellationToken cancelToken, Action onProgress) { + _service = service; _repo = repo; _changes = changes; _cancelToken = cancelToken; @@ -75,7 +76,7 @@ namespace SourceGit.Commands var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd(); var diff = rs.IsSuccess ? rs.StdOut : "unknown change"; - var rsp = Models.OpenAI.Chat(Models.OpenAI.AnalyzeDiffPrompt, $"Here is the `git diff` output: {diff}", _cancelToken); + var rsp = _service.Chat(_service.AnalyzeDiffPrompt, $"Here is the `git diff` output: {diff}", _cancelToken); if (rsp != null && rsp.Choices.Count > 0) return rsp.Choices[0].Message.Content; @@ -84,13 +85,14 @@ namespace SourceGit.Commands private string GenerateSubject(string summary) { - var rsp = Models.OpenAI.Chat(Models.OpenAI.GenerateSubjectPrompt, $"Here are the summaries changes:\n{summary}", _cancelToken); + var rsp = _service.Chat(_service.GenerateSubjectPrompt, $"Here are the summaries changes:\n{summary}", _cancelToken); if (rsp != null && rsp.Choices.Count > 0) return rsp.Choices[0].Message.Content; return string.Empty; } + private Models.OpenAIService _service; private string _repo; private List _changes; private CancellationToken _cancelToken; diff --git a/src/Models/OpenAI.cs b/src/Models/OpenAI.cs index b1bb9465..e9c7b5ed 100644 --- a/src/Models/OpenAI.cs +++ b/src/Models/OpenAI.cs @@ -6,6 +6,8 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; +using CommunityToolkit.Mvvm.ComponentModel; + namespace SourceGit.Models { public class OpenAIChatMessage @@ -74,44 +76,78 @@ namespace SourceGit.Models } } - public static class OpenAI + public class OpenAIService : ObservableObject { - public static string Server + public string Name { - get; - set; + get => _name; + set => SetProperty(ref _name, value); } - public static string ApiKey + public string Server { - get; - set; + get => _server; + set => SetProperty(ref _server, value); } - public static string Model + public string ApiKey { - get; - set; + get => _apiKey; + set => SetProperty(ref _apiKey, value); } - public static string AnalyzeDiffPrompt + public string Model { - get; - set; + get => _model; + set => SetProperty(ref _model, value); } - public static string GenerateSubjectPrompt + public string AnalyzeDiffPrompt { - get; - set; + get => _analyzeDiffPrompt; + set => SetProperty(ref _analyzeDiffPrompt, value); } - public static bool IsValid + public string GenerateSubjectPrompt { - get => !string.IsNullOrEmpty(Server) && !string.IsNullOrEmpty(Model); + get => _generateSubjectPrompt; + set => SetProperty(ref _generateSubjectPrompt, value); } - public static OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation) + public OpenAIService() + { + AnalyzeDiffPrompt = """ + You are an expert developer specialist in creating commits. + Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules: + - Do not use any code snippets, imports, file routes or bullets points. + - Do not mention the route of file that has been change. + - Write clear, concise, and descriptive messages that explain the MAIN GOAL made of the changes. + - Use the present tense and active voice in the message, for example, "Fix bug" instead of "Fixed bug.". + - Use the imperative mood, which gives the message a sense of command, e.g. "Add feature" instead of "Added feature". + - Avoid using general terms like "update" or "change", be specific about what was updated or changed. + - Avoid using terms like "The main goal of", just output directly the summary in plain text + """; + + GenerateSubjectPrompt = """ + You are an expert developer specialist in creating commits messages. + Your only goal is to retrieve a single commit message. + Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules: + - Assign the commit {type} according to the next conditions: + feat: Only when adding a new feature. + fix: When fixing a bug. + docs: When updating documentation. + style: When changing elements styles or design and/or making changes to the code style (formatting, missing semicolons, etc.) without changing the code logic. + test: When adding or updating tests. + chore: When making changes to the build process or auxiliary tools and libraries. + revert: When undoing a previous commit. + refactor: When restructuring code without changing its external behavior, or is any of the other refactor types. + - Do not add any issues numeration, explain your output nor introduce your answer. + - Output directly only one commit message in plain text with the next format: {type}: {commit_message}. + - Be as concise as possible, keep the message under 50 characters. + """; + } + + public OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation) { var chat = new OpenAIChatRequest() { Model = Model }; chat.AddMessage("system", prompt); @@ -144,5 +180,12 @@ namespace SourceGit.Models throw; } } + + private string _name; + private string _server; + private string _apiKey; + private string _model; + private string _analyzeDiffPrompt; + private string _generateSubjectPrompt; } } diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index ff846035..ae386385 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -401,12 +401,12 @@ Last year {0} years ago Preference - Advanced Options OPEN AI Analyze Diff Prompt API Key Generate Subject Prompt Model + Name Server APPEARANCE Default Font diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 0d7c55fb..f9271a95 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -405,7 +405,6 @@ В пролому году {0} лет назад Параметры - Расширенные опции ОТКРЫТЬ ИИ Ключ API Запрос на анализ различий diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index f4027111..eea6d169 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -404,7 +404,13 @@ 一年前 {0}年前 偏好设置 - 高级设置 + OPEN AI + Analyze Diff Prompt + API密钥 + Generate Subject Prompt + 模型 + 配置名称 + 服务地址 外观配置 缺省字体 默认字体大小 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index a72914f7..5ae1f860 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -403,12 +403,12 @@ {0} 個月前 一年前 {0} 年前 - 偏好設定 進階設定 OpenAI 伺服器 API 金鑰 模型 + 名稱 分析變更差異提示詞 產生提交訊息提示詞 外觀設定 diff --git a/src/ViewModels/Preference.cs b/src/ViewModels/Preference.cs index 72667f54..2741650c 100644 --- a/src/ViewModels/Preference.cs +++ b/src/ViewModels/Preference.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Text.Json; using System.Text.Json.Serialization; - +using Avalonia.Collections; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -25,7 +25,6 @@ namespace SourceGit.ViewModels _instance.PrepareGit(); _instance.PrepareShellOrTerminal(); _instance.PrepareWorkspaces(); - _instance.PrepareOpenAIPrompt(); return _instance; } @@ -277,71 +276,6 @@ namespace SourceGit.ViewModels set => SetProperty(ref _externalMergeToolPath, value); } - public string OpenAIServer - { - get => Models.OpenAI.Server; - set - { - if (value != Models.OpenAI.Server) - { - Models.OpenAI.Server = value; - OnPropertyChanged(); - } - } - } - - public string OpenAIApiKey - { - get => Models.OpenAI.ApiKey; - set - { - if (value != Models.OpenAI.ApiKey) - { - Models.OpenAI.ApiKey = value; - OnPropertyChanged(); - } - } - } - - public string OpenAIModel - { - get => Models.OpenAI.Model; - set - { - if (value != Models.OpenAI.Model) - { - Models.OpenAI.Model = value; - OnPropertyChanged(); - } - } - } - - public string OpenAIAnalyzeDiffPrompt - { - get => Models.OpenAI.AnalyzeDiffPrompt; - set - { - if (value != Models.OpenAI.AnalyzeDiffPrompt) - { - Models.OpenAI.AnalyzeDiffPrompt = value; - OnPropertyChanged(); - } - } - } - - public string OpenAIGenerateSubjectPrompt - { - get => Models.OpenAI.GenerateSubjectPrompt; - set - { - if (value != Models.OpenAI.GenerateSubjectPrompt) - { - Models.OpenAI.GenerateSubjectPrompt = value; - OnPropertyChanged(); - } - } - } - public uint StatisticsSampleColor { get => _statisticsSampleColor; @@ -360,6 +294,12 @@ namespace SourceGit.ViewModels set; } = []; + public AvaloniaList OpenAIServices + { + get; + set; + } = []; + public double LastCheckUpdateTime { get => _lastCheckUpdateTime; @@ -554,45 +494,6 @@ namespace SourceGit.ViewModels } } - private void PrepareOpenAIPrompt() - { - if (string.IsNullOrEmpty(Models.OpenAI.AnalyzeDiffPrompt)) - { - Models.OpenAI.AnalyzeDiffPrompt = """ - You are an expert developer specialist in creating commits. - Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules: - - Do not use any code snippets, imports, file routes or bullets points. - - Do not mention the route of file that has been change. - - Write clear, concise, and descriptive messages that explain the MAIN GOAL made of the changes. - - Use the present tense and active voice in the message, for example, "Fix bug" instead of "Fixed bug.". - - Use the imperative mood, which gives the message a sense of command, e.g. "Add feature" instead of "Added feature". - - Avoid using general terms like "update" or "change", be specific about what was updated or changed. - - Avoid using terms like "The main goal of", just output directly the summary in plain text - """; - } - - if (string.IsNullOrEmpty(Models.OpenAI.GenerateSubjectPrompt)) - { - Models.OpenAI.GenerateSubjectPrompt = """ - You are an expert developer specialist in creating commits messages. - Your only goal is to retrieve a single commit message. - Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules: - - Assign the commit {type} according to the next conditions: - feat: Only when adding a new feature. - fix: When fixing a bug. - docs: When updating documentation. - style: When changing elements styles or design and/or making changes to the code style (formatting, missing semicolons, etc.) without changing the code logic. - test: When adding or updating tests. - chore: When making changes to the build process or auxiliary tools and libraries. - revert: When undoing a previous commit. - refactor: When restructuring code without changing its external behavior, or is any of the other refactor types. - - Do not add any issues numeration, explain your output nor introduce your answer. - - Output directly only one commit message in plain text with the next format: {type}: {commit_message}. - - Be as concise as possible, keep the message under 50 characters. - """; - } - } - private RepositoryNode FindNodeRecursive(string id, List collection) { foreach (var node in collection) diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 6fcbedd9..6393c6ad 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -403,25 +403,6 @@ namespace SourceGit.ViewModels } } - public void GenerateCommitMessageByAI() - { - if (!Models.OpenAI.IsValid) - { - App.RaiseException(_repo.FullPath, "Bad configuration for OpenAI"); - return; - } - - if (_staged is { Count: > 0 }) - { - var dialog = new Views.AIAssistant(_repo.FullPath, _staged, generated => CommitMessage = generated); - App.OpenDialog(dialog); - } - else - { - App.RaiseException(_repo.FullPath, "No files added to commit!"); - } - } - public void Commit() { DoCommit(false, false, false); @@ -1211,6 +1192,51 @@ namespace SourceGit.ViewModels return menu; } + public ContextMenu CreateContextForOpenAI() + { + if (_staged == null || _staged.Count == 0) + { + App.RaiseException(_repo.FullPath, "No files added to commit!"); + return null; + } + + var services = Preference.Instance.OpenAIServices; + if (services.Count == 0) + { + App.RaiseException(_repo.FullPath, "Bad configuration for OpenAI"); + return null; + } + + if (services.Count == 1) + { + var dialog = new Views.AIAssistant(services[0], _repo.FullPath, _staged, generated => CommitMessage = generated); + App.OpenDialog(dialog); + return null; + } + else + { + var menu = new ContextMenu() { Placement = PlacementMode.TopEdgeAlignedLeft }; + + foreach (var service in services) + { + var dup = service; + + var item = new MenuItem(); + item.Header = service.Name; + item.Click += (_, e) => + { + var dialog = new Views.AIAssistant(dup, _repo.FullPath, _staged, generated => CommitMessage = generated); + App.OpenDialog(dialog); + e.Handled = true; + }; + + menu.Items.Add(item); + } + + return menu; + } + } + private List GetStagedChanges() { if (_useAmend) diff --git a/src/Views/AIAssistant.axaml.cs b/src/Views/AIAssistant.axaml.cs index 6ceb5610..331d380d 100644 --- a/src/Views/AIAssistant.axaml.cs +++ b/src/Views/AIAssistant.axaml.cs @@ -17,12 +17,14 @@ namespace SourceGit.Views InitializeComponent(); } - public AIAssistant(string repo, List changes, Action onDone) + public AIAssistant(Models.OpenAIService service, string repo, List changes, Action onDone) { + _service = service; _repo = repo; _changes = changes; _onDone = onDone; _cancel = new CancellationTokenSource(); + InitializeComponent(); } @@ -35,7 +37,7 @@ namespace SourceGit.Views Task.Run(() => { - var message = new Commands.GenerateCommitMessage(_repo, _changes, _cancel.Token, SetDescription).Result(); + var message = new Commands.GenerateCommitMessage(_service, _repo, _changes, _cancel.Token, SetDescription).Result(); if (_cancel.IsCancellationRequested) return; @@ -63,6 +65,7 @@ namespace SourceGit.Views Dispatcher.UIThread.Invoke(() => ProgressMessage.Text = message); } + private Models.OpenAIService _service; private string _repo; private List _changes; private Action _onDone; diff --git a/src/Views/Preference.axaml b/src/Views/Preference.axaml index b95dbe66..63d5ff68 100644 --- a/src/Views/Preference.axaml +++ b/src/Views/Preference.axaml @@ -412,7 +412,7 @@ - + @@ -459,82 +459,117 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Preference.axaml.cs b/src/Views/Preference.axaml.cs index 2f08e0db..f7cabec1 100644 --- a/src/Views/Preference.axaml.cs +++ b/src/Views/Preference.axaml.cs @@ -74,6 +74,15 @@ namespace SourceGit.Views set; } + public static readonly StyledProperty SelectedOpenAIServiceProperty = + AvaloniaProperty.Register(nameof(SelectedOpenAIService)); + + public Models.OpenAIService SelectedOpenAIService + { + get => GetValue(SelectedOpenAIServiceProperty); + set => SetValue(SelectedOpenAIServiceProperty, value); + } + public Preference() { var pref = ViewModels.Preference.Instance; @@ -312,5 +321,24 @@ namespace SourceGit.Views e.Handled = true; } + + private void OnAddOpenAIService(object sender, RoutedEventArgs e) + { + var service = new Models.OpenAIService() { Name = "Unnamed Service" }; + ViewModels.Preference.Instance.OpenAIServices.Add(service); + SelectedOpenAIService = service; + + e.Handled = true; + } + + private void OnRemoveSelectedOpenAIService(object sender, RoutedEventArgs e) + { + if (SelectedOpenAIService == null) + return; + + ViewModels.Preference.Instance.OpenAIServices.Remove(SelectedOpenAIService); + SelectedOpenAIService = null; + e.Handled = true; + } } } diff --git a/src/Views/WorkingCopy.axaml b/src/Views/WorkingCopy.axaml index 3d080fc6..9fab9927 100644 --- a/src/Views/WorkingCopy.axaml +++ b/src/Views/WorkingCopy.axaml @@ -199,7 +199,7 @@