Skip to content

Commit a915708

Browse files
committed
refactor: rewrite OpenAI integration
- use `OpenAI` and `Azure.AI.OpenAI` - use `developer` role instead of `system` for OpenAI's `o1` series models - use streaming response - re-design `AIAssistant` Signed-off-by: leo <[email protected]>
1 parent cf90e51 commit a915708

File tree

12 files changed

+269
-211
lines changed

12 files changed

+269
-211
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,11 @@ This software supports using OpenAI or other AI service that has an OpenAI comap
136136

137137
For `OpenAI`:
138138

139-
* `Server` must be `https://api.openai.com/v1/chat/completions`
139+
* `Server` must be `https://api.openai.com/v1`
140140

141141
For other AI service:
142142

143-
* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1/chat/completions`. For example, when using `Ollama`, it should be `http://localhost:11434/v1/chat/completions` instead of `http://localhost:11434/api/generate`
143+
* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1`. For example, when using `Ollama`, it should be `http://localhost:11434/v1` instead of `http://localhost:11434/api/generate`
144144
* The `API Key` is optional that depends on the service
145145

146146
## External Tools

src/App.JsonCodeGen.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ public override void Write(Utf8JsonWriter writer, GridLength value, JsonSerializ
4646
[JsonSerializable(typeof(Models.ExternalToolPaths))]
4747
[JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))]
4848
[JsonSerializable(typeof(Models.JetBrainsState))]
49-
[JsonSerializable(typeof(Models.OpenAIChatRequest))]
50-
[JsonSerializable(typeof(Models.OpenAIChatResponse))]
5149
[JsonSerializable(typeof(Models.ThemeOverrides))]
5250
[JsonSerializable(typeof(Models.Version))]
5351
[JsonSerializable(typeof(Models.RepositorySettings))]

src/Commands/GenerateCommitMessage.cs

Lines changed: 42 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using System.Text;
44
using System.Threading;
55

6+
using Avalonia.Threading;
7+
68
namespace SourceGit.Commands
79
{
810
/// <summary>
@@ -20,82 +22,75 @@ public GetDiffContent(string repo, Models.DiffOption opt)
2022
}
2123
}
2224

23-
public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onProgress)
25+
public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onResponse)
2426
{
2527
_service = service;
2628
_repo = repo;
2729
_changes = changes;
2830
_cancelToken = cancelToken;
29-
_onProgress = onProgress;
31+
_onResponse = onResponse;
3032
}
3133

32-
public string Result()
34+
public void Exec()
3335
{
3436
try
3537
{
36-
var summarybuilder = new StringBuilder();
37-
var bodyBuilder = new StringBuilder();
38+
var responseBuilder = new StringBuilder();
39+
var summaryBuilder = new StringBuilder();
3840
foreach (var change in _changes)
3941
{
4042
if (_cancelToken.IsCancellationRequested)
41-
return "";
43+
return;
4244

43-
_onProgress?.Invoke($"Analyzing {change.Path}...");
45+
responseBuilder.Append("- ");
46+
summaryBuilder.Append("- ");
4447

45-
var summary = GenerateChangeSummary(change);
46-
summarybuilder.Append("- ");
47-
summarybuilder.Append(summary);
48-
summarybuilder.Append("(file: ");
49-
summarybuilder.Append(change.Path);
50-
summarybuilder.Append(")");
51-
summarybuilder.AppendLine();
48+
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
49+
if (rs.IsSuccess)
50+
{
51+
_service.Chat(
52+
_service.AnalyzeDiffPrompt,
53+
$"Here is the `git diff` output: {rs.StdOut}",
54+
_cancelToken,
55+
update =>
56+
{
57+
responseBuilder.Append(update);
58+
summaryBuilder.Append(update);
59+
_onResponse?.Invoke("Waiting for pre-file analyzing to complated...\n\n" + responseBuilder.ToString());
60+
});
61+
}
5262

53-
bodyBuilder.Append("- ");
54-
bodyBuilder.Append(summary);
55-
bodyBuilder.AppendLine();
63+
responseBuilder.Append("\n");
64+
summaryBuilder.Append("(file: ");
65+
summaryBuilder.Append(change.Path);
66+
summaryBuilder.Append(")\n");
5667
}
5768

5869
if (_cancelToken.IsCancellationRequested)
59-
return "";
60-
61-
_onProgress?.Invoke($"Generating commit message...");
70+
return;
6271

63-
var body = bodyBuilder.ToString();
64-
var subject = GenerateSubject(summarybuilder.ToString());
65-
return string.Format("{0}\n\n{1}", subject, body);
72+
var responseBody = responseBuilder.ToString();
73+
var subjectBuilder = new StringBuilder();
74+
_service.Chat(
75+
_service.GenerateSubjectPrompt,
76+
$"Here are the summaries changes:\n{summaryBuilder}",
77+
_cancelToken,
78+
update =>
79+
{
80+
subjectBuilder.Append(update);
81+
_onResponse?.Invoke($"{subjectBuilder}\n\n{responseBody}");
82+
});
6683
}
6784
catch (Exception e)
6885
{
69-
App.RaiseException(_repo, $"Failed to generate commit message: {e}");
70-
return "";
86+
Dispatcher.UIThread.Post(() => App.RaiseException(_repo, $"Failed to generate commit message: {e}"));
7187
}
7288
}
7389

74-
private string GenerateChangeSummary(Models.Change change)
75-
{
76-
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
77-
var diff = rs.IsSuccess ? rs.StdOut : "unknown change";
78-
79-
var rsp = _service.Chat(_service.AnalyzeDiffPrompt, $"Here is the `git diff` output: {diff}", _cancelToken);
80-
if (rsp != null && rsp.Choices.Count > 0)
81-
return rsp.Choices[0].Message.Content;
82-
83-
return string.Empty;
84-
}
85-
86-
private string GenerateSubject(string summary)
87-
{
88-
var rsp = _service.Chat(_service.GenerateSubjectPrompt, $"Here are the summaries changes:\n{summary}", _cancelToken);
89-
if (rsp != null && rsp.Choices.Count > 0)
90-
return rsp.Choices[0].Message.Content;
91-
92-
return string.Empty;
93-
}
94-
9590
private Models.OpenAIService _service;
9691
private string _repo;
9792
private List<Models.Change> _changes;
9893
private CancellationToken _cancelToken;
99-
private Action<string> _onProgress;
94+
private Action<string> _onResponse;
10095
}
10196
}

src/Models/OpenAI.cs

Lines changed: 31 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,13 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.Net.Http;
4-
using System.Text;
5-
using System.Text.Json;
6-
using System.Text.Json.Serialization;
2+
using System.ClientModel;
73
using System.Threading;
8-
4+
using Azure.AI.OpenAI;
95
using CommunityToolkit.Mvvm.ComponentModel;
6+
using OpenAI;
7+
using OpenAI.Chat;
108

119
namespace SourceGit.Models
1210
{
13-
public class OpenAIChatMessage
14-
{
15-
[JsonPropertyName("role")]
16-
public string Role
17-
{
18-
get;
19-
set;
20-
}
21-
22-
[JsonPropertyName("content")]
23-
public string Content
24-
{
25-
get;
26-
set;
27-
}
28-
}
29-
30-
public class OpenAIChatChoice
31-
{
32-
[JsonPropertyName("index")]
33-
public int Index
34-
{
35-
get;
36-
set;
37-
}
38-
39-
[JsonPropertyName("message")]
40-
public OpenAIChatMessage Message
41-
{
42-
get;
43-
set;
44-
}
45-
}
46-
47-
public class OpenAIChatResponse
48-
{
49-
[JsonPropertyName("choices")]
50-
public List<OpenAIChatChoice> Choices
51-
{
52-
get;
53-
set;
54-
} = [];
55-
}
56-
57-
public class OpenAIChatRequest
58-
{
59-
[JsonPropertyName("model")]
60-
public string Model
61-
{
62-
get;
63-
set;
64-
}
65-
66-
[JsonPropertyName("messages")]
67-
public List<OpenAIChatMessage> Messages
68-
{
69-
get;
70-
set;
71-
} = [];
72-
73-
public void AddMessage(string role, string content)
74-
{
75-
Messages.Add(new OpenAIChatMessage { Role = role, Content = content });
76-
}
77-
}
78-
7911
public class OpenAIService : ObservableObject
8012
{
8113
public string Name
@@ -147,48 +79,48 @@ Your only goal is to retrieve a single commit message.
14779
""";
14880
}
14981

150-
public OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation)
82+
public void Chat(string prompt, string question, CancellationToken cancellation, Action<string> onUpdate)
15183
{
152-
var chat = new OpenAIChatRequest() { Model = Model };
153-
chat.AddMessage("user", prompt);
154-
chat.AddMessage("user", question);
155-
156-
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(60) };
157-
if (!string.IsNullOrEmpty(ApiKey))
84+
Uri server = new(Server);
85+
ApiKeyCredential key = new(ApiKey);
86+
ChatClient client = null;
87+
if (Server.Contains("openai.azure.com/", StringComparison.Ordinal))
15888
{
159-
if (Server.Contains("openai.azure.com/", StringComparison.Ordinal))
160-
client.DefaultRequestHeaders.Add("api-key", ApiKey);
161-
else
162-
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
89+
var azure = new AzureOpenAIClient(server, key);
90+
client = azure.GetChatClient(Model);
91+
}
92+
else
93+
{
94+
var openai = new OpenAIClient(key, new() { Endpoint = server });
95+
client = openai.GetChatClient(Model);
16396
}
16497

165-
var req = new StringContent(JsonSerializer.Serialize(chat, JsonCodeGen.Default.OpenAIChatRequest), Encoding.UTF8, "application/json");
16698
try
16799
{
168-
var task = client.PostAsync(Server, req, cancellation);
169-
task.Wait(cancellation);
170-
171-
var rsp = task.Result;
172-
var reader = rsp.Content.ReadAsStringAsync(cancellation);
173-
reader.Wait(cancellation);
100+
var updates = client.CompleteChatStreaming([
101+
ShouldUseDeveloperPrompt() ? new DeveloperChatMessage(prompt) : new SystemChatMessage(prompt),
102+
new UserChatMessage(question),
103+
], null, cancellation);
174104

175-
var body = reader.Result;
176-
if (!rsp.IsSuccessStatusCode)
105+
foreach (var update in updates)
177106
{
178-
throw new Exception($"AI service returns error code {rsp.StatusCode}. Body: {body ?? string.Empty}");
107+
if (update.ContentUpdate.Count > 0)
108+
onUpdate.Invoke(update.ContentUpdate[0].Text);
179109
}
180-
181-
return JsonSerializer.Deserialize(reader.Result, JsonCodeGen.Default.OpenAIChatResponse);
182110
}
183111
catch
184112
{
185-
if (cancellation.IsCancellationRequested)
186-
return null;
187-
188-
throw;
113+
if (!cancellation.IsCancellationRequested)
114+
throw;
189115
}
190116
}
191117

118+
private bool ShouldUseDeveloperPrompt()
119+
{
120+
return _model.Equals("o1", StringComparison.Ordinal) ||
121+
_model.Equals("o1-mini", StringComparison.Ordinal);
122+
}
123+
192124
private string _name;
193125
private string _server;
194126
private string _apiKey;

src/Resources/Locales/en_US.axaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">Track Branch:</x:String>
2020
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">Tracking remote branch</x:String>
2121
<x:String x:Key="Text.AIAssistant" xml:space="preserve">AI Assistant</x:String>
22+
<x:String x:Key="Text.AIAssistant.Regen" xml:space="preserve">RE-GENERATE</x:String>
2223
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">Use AI to generate commit message</x:String>
24+
<x:String x:Key="Text.AIAssistant.Use" xml:space="preserve">APPLY AS COMMIT MESSAGE</x:String>
2325
<x:String x:Key="Text.Apply" xml:space="preserve">Patch</x:String>
2426
<x:String x:Key="Text.Apply.Error" xml:space="preserve">Error</x:String>
2527
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">Raise errors and refuses to apply the patch</x:String>

src/Resources/Locales/zh_CN.axaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">跟踪分支</x:String>
2323
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">设置上游跟踪分支</x:String>
2424
<x:String x:Key="Text.AIAssistant" xml:space="preserve">AI助手</x:String>
25+
<x:String x:Key="Text.AIAssistant.Regen" xml:space="preserve">重新生成</x:String>
2526
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">使用AI助手生成提交信息</x:String>
27+
<x:String x:Key="Text.AIAssistant.Use" xml:space="preserve">应用本次生成</x:String>
2628
<x:String x:Key="Text.Apply" xml:space="preserve">应用补丁(apply)</x:String>
2729
<x:String x:Key="Text.Apply.Error" xml:space="preserve">错误</x:String>
2830
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">输出错误,并终止应用补丁</x:String>

src/Resources/Locales/zh_TW.axaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">追蹤分支</x:String>
2323
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">設定遠端追蹤分支</x:String>
2424
<x:String x:Key="Text.AIAssistant" xml:space="preserve">AI 助理</x:String>
25+
<x:String x:Key="Text.AIAssistant.Regen" xml:space="preserve">重新產生</x:String>
2526
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">使用 AI 產生提交訊息</x:String>
27+
<x:String x:Key="Text.AIAssistant.Use" xml:space="preserve">套用為提交訊息</x:String>
2628
<x:String x:Key="Text.Apply" xml:space="preserve">套用修補檔 (apply patch)</x:String>
2729
<x:String x:Key="Text.Apply.Error" xml:space="preserve">錯誤</x:String>
2830
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">輸出錯誤,並中止套用修補檔</x:String>

src/SourceGit.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@
4848
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.3" Condition="'$(Configuration)' == 'Debug'" />
4949
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.1.0" />
5050
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" />
51+
<PackageReference Include="Azure.AI.OpenAI" Version="2.1.0" />
5152
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
5253
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0-rc5.1" />
54+
<PackageReference Include="OpenAI" Version="2.2.0-beta.1" />
5355
<PackageReference Include="TextMateSharp" Version="1.0.65" />
5456
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.65" />
5557
</ItemGroup>

src/ViewModels/Repository.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,25 @@ public void OpenWorktree(Models.Worktree worktree)
11991199
App.GetLauncer()?.OpenRepositoryInTab(node, null);
12001200
}
12011201

1202+
public AvaloniaList<Models.OpenAIService> GetPreferedOpenAIServices()
1203+
{
1204+
var services = Preferences.Instance.OpenAIServices;
1205+
if (services == null || services.Count == 0)
1206+
return [];
1207+
1208+
if (services.Count == 1)
1209+
return services;
1210+
1211+
var prefered = _settings.PreferedOpenAIService;
1212+
foreach (var service in services)
1213+
{
1214+
if (service.Name.Equals(prefered, StringComparison.Ordinal))
1215+
return [service];
1216+
}
1217+
1218+
return services;
1219+
}
1220+
12021221
public ContextMenu CreateContextMenuForGitFlow()
12031222
{
12041223
var menu = new ContextMenu();

0 commit comments

Comments
 (0)