From f2a2c09b18b62069f36c87224ab20c3217cb8141 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 25 Mar 2026 11:21:22 +0800 Subject: [PATCH] refactor: move `Models.AIProvider` to `AI.Service` Signed-off-by: leo --- src/AI/Agent.cs | 84 +++++++++++++++++++++++++++++++++ src/AI/ChatTools.cs | 2 +- src/AI/Service.cs | 85 +++------------------------------- src/Models/AIProvider.cs | 12 ----- src/ViewModels/AIAssistant.cs | 10 ++-- src/ViewModels/Preferences.cs | 2 +- src/ViewModels/Repository.cs | 4 +- src/Views/Preferences.axaml | 5 +- src/Views/Preferences.axaml.cs | 8 ++-- 9 files changed, 107 insertions(+), 105 deletions(-) create mode 100644 src/AI/Agent.cs delete mode 100644 src/Models/AIProvider.cs diff --git a/src/AI/Agent.cs b/src/AI/Agent.cs new file mode 100644 index 00000000..3e4e221b --- /dev/null +++ b/src/AI/Agent.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using OpenAI; +using OpenAI.Chat; + +namespace SourceGit.AI +{ + public class Agent + { + public Agent(Service service) + { + _service = service; + } + + public async Task GenerateCommitMessage(string repo, string changeList, Action onUpdate, CancellationToken cancellation) + { + var endPoint = new Uri(_service.Server); + var client = _service.Server.Contains("openai.azure.com/", StringComparison.Ordinal) + ? new AzureOpenAIClient(endPoint, _service.Credential) + : new OpenAIClient(_service.Credential, new() { Endpoint = endPoint }); + + var chatClient = client.GetChatClient(_service.Model); + var options = new ChatCompletionOptions() { Tools = { ChatTools.Tool_GetDetailChangesInFile } }; + + var userMessageBuilder = new StringBuilder(); + userMessageBuilder + .AppendLine("Generate a commit message (follow the rule of conventional commit message) for given git repository.") + .AppendLine("- Read all given changed files before generating. Do not skip any one file.") + .AppendLine("- Output the conventional commit message (with detail changes in list) directly. Do not explain your output nor introduce your answer.") + .AppendLine(string.IsNullOrEmpty(_service.AdditionalPrompt) ? string.Empty : _service.AdditionalPrompt) + .Append("Reposiory path: ").AppendLine(repo.Quoted()) + .AppendLine("Changed files: ") + .Append(changeList); + + var messages = new List() { new UserChatMessage(userMessageBuilder.ToString()) }; + + do + { + ChatCompletion completion = await chatClient.CompleteChatAsync(messages, options, cancellation); + var inProgress = false; + + switch (completion.FinishReason) + { + case ChatFinishReason.Stop: + onUpdate?.Invoke(string.Empty); + onUpdate?.Invoke("[Assistant]:"); + if (completion.Content.Count > 0) + onUpdate?.Invoke(completion.Content[0].Text); + else + onUpdate?.Invoke("[No content was generated.]"); + break; + case ChatFinishReason.Length: + throw new Exception("The response was cut off because it reached the maximum length. Consider increasing the max tokens limit."); + case ChatFinishReason.ToolCalls: + { + messages.Add(new AssistantChatMessage(completion)); + + foreach (var call in completion.ToolCalls) + { + var result = await ChatTools.Process(call, onUpdate); + messages.Add(result); + } + + inProgress = true; + break; + } + case ChatFinishReason.ContentFilter: + throw new Exception("Ommitted content due to a content filter flag"); + default: + break; + } + + if (!inProgress) + break; + } while (true); + } + + private readonly Service _service; + } +} diff --git a/src/AI/ChatTools.cs b/src/AI/ChatTools.cs index 389100e9..0278e54e 100644 --- a/src/AI/ChatTools.cs +++ b/src/AI/ChatTools.cs @@ -25,7 +25,7 @@ namespace SourceGit.AI }, "originalFile": { "type": "string", - "description": "The path to the original file when it has been renamed." + "description": "The path to the original file when it has been renamed (marked as 'R' or 'C')." } }, "required": ["repo", "file"] diff --git a/src/AI/Service.cs b/src/AI/Service.cs index 2a3daed0..2482a8f8 100644 --- a/src/AI/Service.cs +++ b/src/AI/Service.cs @@ -1,87 +1,16 @@ using System; using System.ClientModel; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using OpenAI; -using OpenAI.Chat; namespace SourceGit.AI { public class Service { - public Service(Models.AIProvider ai) - { - _ai = ai; - } - - public async Task GenerateCommitMessage(string repo, string changeList, Action onUpdate, CancellationToken cancellation) - { - var key = _ai.ReadApiKeyFromEnv ? Environment.GetEnvironmentVariable(_ai.ApiKey) : _ai.ApiKey; - var endPoint = new Uri(_ai.Server); - var credential = new ApiKeyCredential(key); - var client = _ai.Server.Contains("openai.azure.com/", StringComparison.Ordinal) - ? new AzureOpenAIClient(endPoint, credential) - : new OpenAIClient(credential, new() { Endpoint = endPoint }); - - var chatClient = client.GetChatClient(_ai.Model); - var options = new ChatCompletionOptions() { Tools = { ChatTools.Tool_GetDetailChangesInFile } }; - - var userMessageBuilder = new StringBuilder(); - userMessageBuilder - .AppendLine("Generate a commit message (follow the rule of conventional commit message) for given git repository.") - .AppendLine("- Read all given changed files before generating. Do not skip any one file.") - .AppendLine("- Output the conventional commit message (with detail changes in list) directly. Do not explain your output nor introduce your answer.") - .AppendLine(string.IsNullOrEmpty(_ai.AdditionalPrompt) ? string.Empty : _ai.AdditionalPrompt) - .Append("Reposiory path: ").AppendLine(repo.Quoted()) - .AppendLine("Changed files: ") - .Append(changeList); - - var messages = new List() { new UserChatMessage(userMessageBuilder.ToString()) }; - - do - { - ChatCompletion completion = await chatClient.CompleteChatAsync(messages, options, cancellation); - var inProgress = false; - - switch (completion.FinishReason) - { - case ChatFinishReason.Stop: - onUpdate?.Invoke(string.Empty); - onUpdate?.Invoke("[Assistant]:"); - if (completion.Content.Count > 0) - onUpdate?.Invoke(completion.Content[0].Text); - else - onUpdate?.Invoke("[No content was generated.]"); - break; - case ChatFinishReason.Length: - throw new Exception("The response was cut off because it reached the maximum length. Consider increasing the max tokens limit."); - case ChatFinishReason.ToolCalls: - { - messages.Add(new AssistantChatMessage(completion)); - - foreach (var call in completion.ToolCalls) - { - var result = await ChatTools.Process(call, onUpdate); - messages.Add(result); - } - - inProgress = true; - break; - } - case ChatFinishReason.ContentFilter: - throw new Exception("Ommitted content due to a content filter flag"); - default: - break; - } - - if (!inProgress) - break; - } while (true); - } - - private readonly Models.AIProvider _ai; + public string Name { get; set; } + public string Server { get; set; } + public string Model { get; set; } + public string ApiKey { get; set; } + public bool ReadApiKeyFromEnv { get; set; } + public string AdditionalPrompt { get; set; } + public ApiKeyCredential Credential => new ApiKeyCredential(ReadApiKeyFromEnv ? Environment.GetEnvironmentVariable(ApiKey) : ApiKey); } } diff --git a/src/Models/AIProvider.cs b/src/Models/AIProvider.cs deleted file mode 100644 index e44f054f..00000000 --- a/src/Models/AIProvider.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace SourceGit.Models -{ - public class AIProvider - { - public string Name { get; set; } - public string Server { get; set; } - public string Model { get; set; } - public string ApiKey { get; set; } - public bool ReadApiKeyFromEnv { get; set; } - public string AdditionalPrompt { get; set; } - } -} diff --git a/src/ViewModels/AIAssistant.cs b/src/ViewModels/AIAssistant.cs index 07d89c20..96e32a03 100644 --- a/src/ViewModels/AIAssistant.cs +++ b/src/ViewModels/AIAssistant.cs @@ -24,10 +24,10 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _text, value); } - public AIAssistant(string repo, Models.AIProvider provider, List changes) + public AIAssistant(string repo, AI.Service service, List changes) { _repo = repo; - _provider = provider; + _service = service; _cancel = new CancellationTokenSource(); var builder = new StringBuilder(); @@ -80,14 +80,14 @@ namespace SourceGit.ViewModels _cancel = new CancellationTokenSource(); Task.Run(async () => { - var server = new AI.Service(_provider); + var agent = new AI.Agent(_service); var builder = new StringBuilder(); builder.AppendLine("Asking AI to generate commit message...").AppendLine(); Dispatcher.UIThread.Post(() => Text = builder.ToString()); try { - await server.GenerateCommitMessage(_repo, _changeList, message => + await agent.GenerateCommitMessage(_repo, _changeList, message => { builder.AppendLine(message); Dispatcher.UIThread.Post(() => Text = builder.ToString()); @@ -103,7 +103,7 @@ namespace SourceGit.ViewModels } private readonly string _repo = null; - private readonly Models.AIProvider _provider = null; + private readonly AI.Service _service = null; private readonly string _changeList = null; private CancellationTokenSource _cancel = null; private bool _isGenerating = false; diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index 520fc560..b71563db 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -480,7 +480,7 @@ namespace SourceGit.ViewModels set; } = []; - public AvaloniaList OpenAIServices + public AvaloniaList OpenAIServices { get; set; diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index eecbfc85..e39e29cd 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1599,7 +1599,7 @@ namespace SourceGit.ViewModels log.Complete(); } - public List GetPreferredOpenAIServices() + public List GetPreferredOpenAIServices() { var services = Preferences.Instance.OpenAIServices; if (services == null || services.Count == 0) @@ -1609,7 +1609,7 @@ namespace SourceGit.ViewModels return [services[0]]; var preferred = _settings.PreferredOpenAIService; - var all = new List(); + var all = new List(); foreach (var service in services) { if (service.Name.Equals(preferred, StringComparison.Ordinal)) diff --git a/src/Views/Preferences.axaml b/src/Views/Preferences.axaml index 1654b683..0a4681ed 100644 --- a/src/Views/Preferences.axaml +++ b/src/Views/Preferences.axaml @@ -3,6 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:s="using:SourceGit" + xmlns:ai="using:SourceGit.AI" xmlns:m="using:SourceGit.Models" xmlns:c="using:SourceGit.Converters" xmlns:vm="using:SourceGit.ViewModels" @@ -822,7 +823,7 @@ - + @@ -859,7 +860,7 @@ - + diff --git a/src/Views/Preferences.axaml.cs b/src/Views/Preferences.axaml.cs index 53b51db0..20fe37d4 100644 --- a/src/Views/Preferences.axaml.cs +++ b/src/Views/Preferences.axaml.cs @@ -95,10 +95,10 @@ namespace SourceGit.Views set; } = false; - public static readonly StyledProperty SelectedOpenAIServiceProperty = - AvaloniaProperty.Register(nameof(SelectedOpenAIService)); + public static readonly StyledProperty SelectedOpenAIServiceProperty = + AvaloniaProperty.Register(nameof(SelectedOpenAIService)); - public Models.AIProvider SelectedOpenAIService + public AI.Service SelectedOpenAIService { get => GetValue(SelectedOpenAIServiceProperty); set => SetValue(SelectedOpenAIServiceProperty, value); @@ -397,7 +397,7 @@ namespace SourceGit.Views private void OnAddOpenAIService(object sender, RoutedEventArgs e) { - var service = new Models.AIProvider() { Name = "Unnamed Service" }; + var service = new AI.Service() { Name = "Unnamed Service" }; ViewModels.Preferences.Instance.OpenAIServices.Add(service); SelectedOpenAIService = service;