200 lines
7.1 KiB
C#
200 lines
7.1 KiB
C#
using System.Text;
|
|
using MarketAlly.AIPlugin;
|
|
using MarketAlly.AIPlugin.Conversation;
|
|
using MarketAlly.GitCommitEditor.Models;
|
|
using MarketAlly.GitCommitEditor.Options;
|
|
using MarketAlly.GitCommitEditor.Plugins;
|
|
using MarketAlly.GitCommitEditor.Services;
|
|
|
|
namespace MarketAlly.GitCommitEditor.Rewriters;
|
|
|
|
/// <summary>
|
|
/// Commit message rewriter using MarketAlly.AIPlugin's AIConversation API.
|
|
/// Supports multiple AI providers (Claude, OpenAI, Gemini, Mistral, Qwen).
|
|
/// </summary>
|
|
public sealed class AICommitRewriter : ICommitMessageRewriter, IDisposable
|
|
{
|
|
private const string OperationType = "CommitSuggestion";
|
|
private readonly AiOptions _options;
|
|
private readonly AIConversation _conversation;
|
|
private readonly ICostTrackingService? _costTracker;
|
|
|
|
public AICommitRewriter(AiOptions options, ICostTrackingService? costTracker = null)
|
|
{
|
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
|
_costTracker = costTracker;
|
|
|
|
var provider = ParseProvider(options.Provider);
|
|
|
|
_conversation = AIConversationBuilder.Create()
|
|
.UseProvider(provider, options.ApiKey)
|
|
.UseModel(options.Model)
|
|
.WithSystemPrompt(BuildSystemPrompt())
|
|
.WithTemperature(0.3) // Lower temperature for more consistent output
|
|
.WithMaxTokens(options.MaxTokens)
|
|
.WithToolExecutionLimit(3)
|
|
.RegisterPlugin<ReturnCommitMessagePlugin>()
|
|
.Build();
|
|
}
|
|
|
|
private static AIProvider ParseProvider(string? providerName)
|
|
{
|
|
return providerName?.ToLowerInvariant() switch
|
|
{
|
|
"claude" or "anthropic" => AIProvider.Claude,
|
|
"openai" or "gpt" => AIProvider.OpenAI,
|
|
"gemini" or "google" => AIProvider.Gemini,
|
|
"mistral" => AIProvider.Mistral,
|
|
"qwen" or "alibaba" => AIProvider.Qwen,
|
|
_ => AIProvider.Claude // Default to Claude
|
|
};
|
|
}
|
|
|
|
private static string BuildSystemPrompt()
|
|
{
|
|
return """
|
|
You are a git commit message expert. Your task is to improve commit messages following conventional commit format.
|
|
|
|
RULES:
|
|
1. Description: max 60 chars, imperative mood ("add" not "added"), no period, lowercase start
|
|
2. Scope is optional - use it for the area of codebase (e.g., "api", "ui", "auth")
|
|
3. Be specific about WHAT changed based on the files and diff
|
|
4. Add a body ONLY if the change is complex and needs explanation
|
|
5. Body should explain WHY, not repeat WHAT
|
|
|
|
When you receive commit information, call ReturnCommitMessage with the improved message.
|
|
""";
|
|
}
|
|
|
|
public async Task<SuggestionResult> SuggestImprovedMessageAsync(CommitAnalysis analysis, CancellationToken ct = default)
|
|
{
|
|
var prompt = BuildPrompt(analysis);
|
|
|
|
var response = await _conversation.SendForStructuredOutputAsync<CommitMessageResult>(
|
|
prompt,
|
|
"ReturnCommitMessage",
|
|
ct);
|
|
|
|
// Extract cost information from response
|
|
var inputTokens = response.InputTokens;
|
|
var outputTokens = response.OutputTokens;
|
|
var cost = response.EstimatedCost ?? 0m;
|
|
|
|
// Record cost if tracking service is available
|
|
_costTracker?.RecordOperation(OperationType, inputTokens, outputTokens, cost);
|
|
|
|
if (response.StructuredOutput is CommitMessageResult result)
|
|
{
|
|
// Combine subject and body
|
|
var suggestion = string.IsNullOrWhiteSpace(result.Body)
|
|
? result.Subject.Trim()
|
|
: $"{result.Subject.Trim()}\n\n{result.Body.Trim()}";
|
|
|
|
// Check if the suggestion is actually different from original
|
|
if (string.Equals(suggestion, analysis.OriginalMessage, StringComparison.Ordinal))
|
|
{
|
|
return SuggestionResult.FailedWithOriginal(
|
|
analysis.OriginalMessage,
|
|
$"AI returned same message as original. Raw: {response.FinalMessage}",
|
|
inputTokens, outputTokens, cost);
|
|
}
|
|
|
|
return SuggestionResult.Succeeded(suggestion, inputTokens, outputTokens, cost);
|
|
}
|
|
|
|
// AI failed to return structured output
|
|
return SuggestionResult.FailedWithOriginal(
|
|
analysis.OriginalMessage,
|
|
$"No structured output. FinalMessage: {response.FinalMessage ?? "(null)"}",
|
|
inputTokens, outputTokens, cost);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<(CommitAnalysis Analysis, SuggestionResult Result)>> SuggestBatchAsync(
|
|
IEnumerable<CommitAnalysis> analyses,
|
|
IProgress<int>? progress = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var results = new List<(CommitAnalysis, SuggestionResult)>();
|
|
var analysisList = analyses.ToList();
|
|
var processed = 0;
|
|
|
|
foreach (var analysis in analysisList)
|
|
{
|
|
if (ct.IsCancellationRequested) break;
|
|
|
|
try
|
|
{
|
|
var result = await SuggestImprovedMessageAsync(analysis, ct);
|
|
results.Add((analysis, result));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
results.Add((analysis, SuggestionResult.Failed(ex.Message)));
|
|
}
|
|
|
|
processed++;
|
|
progress?.Report(processed);
|
|
|
|
// Rate limiting delay between requests
|
|
if (processed < analysisList.Count && _options.RateLimitDelayMs > 0)
|
|
{
|
|
await Task.Delay(_options.RateLimitDelayMs, ct);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private string BuildPrompt(CommitAnalysis analysis)
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
sb.AppendLine("Please improve this commit message:");
|
|
sb.AppendLine();
|
|
sb.AppendLine($"ORIGINAL MESSAGE: {analysis.OriginalMessage}");
|
|
sb.AppendLine();
|
|
sb.AppendLine("FILES CHANGED:");
|
|
|
|
var files = analysis.FilesChanged.Take(20).ToList();
|
|
foreach (var file in files)
|
|
{
|
|
sb.AppendLine($" - {file}");
|
|
}
|
|
if (analysis.FilesChanged.Count > 20)
|
|
{
|
|
sb.AppendLine($" ... and {analysis.FilesChanged.Count - 20} more files");
|
|
}
|
|
|
|
sb.AppendLine();
|
|
sb.AppendLine($"STATS: +{analysis.LinesAdded} -{analysis.LinesDeleted} lines");
|
|
|
|
if (!string.IsNullOrEmpty(analysis.DiffSummary) && _options.IncludeDiffContext)
|
|
{
|
|
sb.AppendLine();
|
|
sb.AppendLine("DIFF SUMMARY:");
|
|
var diffLines = analysis.DiffSummary.Split('\n').Take(_options.MaxDiffLines);
|
|
sb.AppendLine(string.Join('\n', diffLines));
|
|
}
|
|
|
|
if (analysis.Quality.Issues.Count > 0)
|
|
{
|
|
sb.AppendLine();
|
|
sb.AppendLine("ISSUES WITH CURRENT MESSAGE:");
|
|
foreach (var issue in analysis.Quality.Issues)
|
|
{
|
|
sb.AppendLine($" - [{issue.Severity}] {issue.Message}");
|
|
}
|
|
}
|
|
|
|
sb.AppendLine();
|
|
sb.AppendLine("Call ReturnCommitMessage with the improved message.");
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// AIConversation handles its own disposal
|
|
}
|
|
}
|