313 lines
10 KiB
C#
313 lines
10 KiB
C#
using System.Text;
|
|
using MarketAlly.AIPlugin;
|
|
using MarketAlly.AIPlugin.Conversation;
|
|
using MarketAlly.GitCommitEditor.Options;
|
|
using MarketAlly.GitCommitEditor.Plugins;
|
|
using MarketAlly.GitCommitEditor.Resources;
|
|
|
|
namespace MarketAlly.GitCommitEditor.Services;
|
|
|
|
/// <summary>
|
|
/// Result of a git diagnosis operation including cost information
|
|
/// </summary>
|
|
public sealed record DiagnosisOperationResult(
|
|
GitDiagnosisResult Diagnosis,
|
|
int InputTokens = 0,
|
|
int OutputTokens = 0,
|
|
decimal EstimatedCost = 0);
|
|
|
|
/// <summary>
|
|
/// Service for diagnosing git issues using AI
|
|
/// </summary>
|
|
public interface IGitDiagnosticService
|
|
{
|
|
/// <summary>
|
|
/// Diagnose a git issue based on the current repository state
|
|
/// </summary>
|
|
Task<GitDiagnosisResult> DiagnoseAsync(string repoPath, string? errorMessage = null, CancellationToken ct = default);
|
|
|
|
/// <summary>
|
|
/// Diagnose a git issue and return cost information
|
|
/// </summary>
|
|
Task<DiagnosisOperationResult> DiagnoseWithCostAsync(string repoPath, string? errorMessage = null, CancellationToken ct = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// AI-powered git diagnostic service using MarketAlly.AIPlugin
|
|
/// </summary>
|
|
public sealed class GitDiagnosticService : IGitDiagnosticService, IDisposable
|
|
{
|
|
private const string OperationType = "GitDiagnosis";
|
|
private readonly AiOptions _options;
|
|
private readonly AIConversation _conversation;
|
|
private readonly ICostTrackingService? _costTracker;
|
|
|
|
public GitDiagnosticService(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.2) // Low temperature for consistent diagnostic output
|
|
.WithMaxTokens(options.MaxTokens)
|
|
.WithToolExecutionLimit(3)
|
|
.RegisterPlugin<ReturnGitDiagnosisPlugin>()
|
|
.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
|
|
};
|
|
}
|
|
|
|
private static string BuildSystemPrompt()
|
|
{
|
|
return """
|
|
You are a git expert who diagnoses and fixes git issues. When given git status, log, and error information, you analyze the problem and provide a clear diagnosis with the exact commands to fix it.
|
|
|
|
COMMON ISSUES YOU CAN DIAGNOSE:
|
|
- Diverged branches (local and remote have different commits)
|
|
- Unrelated histories (refusing to merge unrelated histories)
|
|
- Merge conflicts
|
|
- Detached HEAD state
|
|
- Failed rebases
|
|
- Uncommitted changes blocking operations
|
|
- Authentication issues
|
|
- Remote tracking issues
|
|
- Corrupt repository state
|
|
|
|
RULES:
|
|
1. Always provide the exact command(s) needed to fix the issue
|
|
2. Warn about any potential data loss
|
|
3. Explain WHY the issue happened so the user can avoid it
|
|
4. If multiple solutions exist, recommend the safest one
|
|
5. Set appropriate risk level: low (safe), medium (some risk), high (potential data loss)
|
|
|
|
When you receive git information, analyze it and call ReturnGitDiagnosis with your findings.
|
|
""";
|
|
}
|
|
|
|
public async Task<GitDiagnosisResult> DiagnoseAsync(string repoPath, string? errorMessage = null, CancellationToken ct = default)
|
|
{
|
|
var result = await DiagnoseWithCostAsync(repoPath, errorMessage, ct);
|
|
return result.Diagnosis;
|
|
}
|
|
|
|
public async Task<DiagnosisOperationResult> DiagnoseWithCostAsync(string repoPath, string? errorMessage = null, CancellationToken ct = default)
|
|
{
|
|
var gitInfo = await GatherGitInfoAsync(repoPath);
|
|
var prompt = BuildPrompt(gitInfo, errorMessage);
|
|
|
|
var response = await _conversation.SendForStructuredOutputAsync<GitDiagnosisResult>(
|
|
prompt,
|
|
"ReturnGitDiagnosis",
|
|
ct);
|
|
|
|
// Extract cost information
|
|
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 GitDiagnosisResult result)
|
|
{
|
|
return new DiagnosisOperationResult(result, inputTokens, outputTokens, cost);
|
|
}
|
|
|
|
// Fallback if structured output failed
|
|
var fallback = new GitDiagnosisResult
|
|
{
|
|
Problem = "Unable to diagnose the issue",
|
|
Cause = response.FinalMessage ?? Str.Service_AiAnalysisFailed,
|
|
FixCommand = "git status",
|
|
Warning = "Please review the git state manually",
|
|
RiskLevel = RiskLevel.Low
|
|
};
|
|
return new DiagnosisOperationResult(fallback, inputTokens, outputTokens, cost);
|
|
}
|
|
|
|
private async Task<GitInfo> GatherGitInfoAsync(string repoPath)
|
|
{
|
|
var info = new GitInfo();
|
|
|
|
try
|
|
{
|
|
// Get git status
|
|
info.Status = await RunGitCommandAsync(repoPath, "status");
|
|
|
|
// Get recent log
|
|
info.Log = await RunGitCommandAsync(repoPath, "log --oneline -10");
|
|
|
|
// Get branch info
|
|
info.BranchInfo = await RunGitCommandAsync(repoPath, "branch -vv");
|
|
|
|
// Get remote info
|
|
info.RemoteInfo = await RunGitCommandAsync(repoPath, "remote -v");
|
|
|
|
// Check for ongoing operations
|
|
info.OngoingOps = await CheckOngoingOperationsAsync(repoPath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
info.Error = ex.Message;
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
private static async Task<string> RunGitCommandAsync(string repoPath, string arguments)
|
|
{
|
|
try
|
|
{
|
|
var startInfo = new System.Diagnostics.ProcessStartInfo
|
|
{
|
|
FileName = "git",
|
|
Arguments = arguments,
|
|
WorkingDirectory = repoPath,
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
using var process = System.Diagnostics.Process.Start(startInfo);
|
|
if (process == null) return "[Failed to start git]";
|
|
|
|
var output = await process.StandardOutput.ReadToEndAsync();
|
|
var error = await process.StandardError.ReadToEndAsync();
|
|
await process.WaitForExitAsync();
|
|
|
|
return string.IsNullOrEmpty(error) ? output : $"{output}\n{error}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return $"[Error: {ex.Message}]";
|
|
}
|
|
}
|
|
|
|
private static async Task<string> CheckOngoingOperationsAsync(string repoPath)
|
|
{
|
|
var ops = new List<string>();
|
|
|
|
var gitDir = Path.Combine(repoPath, ".git");
|
|
if (!Directory.Exists(gitDir))
|
|
{
|
|
// Might be a worktree, try to find the git dir
|
|
var gitFile = Path.Combine(repoPath, ".git");
|
|
if (File.Exists(gitFile))
|
|
{
|
|
var content = await File.ReadAllTextAsync(gitFile);
|
|
if (content.StartsWith("gitdir:"))
|
|
{
|
|
gitDir = content.Substring(7).Trim();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Directory.Exists(gitDir))
|
|
{
|
|
if (Directory.Exists(Path.Combine(gitDir, "rebase-merge")) ||
|
|
Directory.Exists(Path.Combine(gitDir, "rebase-apply")))
|
|
{
|
|
ops.Add("REBASE IN PROGRESS");
|
|
}
|
|
|
|
if (File.Exists(Path.Combine(gitDir, "MERGE_HEAD")))
|
|
{
|
|
ops.Add("MERGE IN PROGRESS");
|
|
}
|
|
|
|
if (File.Exists(Path.Combine(gitDir, "CHERRY_PICK_HEAD")))
|
|
{
|
|
ops.Add("CHERRY-PICK IN PROGRESS");
|
|
}
|
|
|
|
if (File.Exists(Path.Combine(gitDir, "REVERT_HEAD")))
|
|
{
|
|
ops.Add("REVERT IN PROGRESS");
|
|
}
|
|
|
|
if (File.Exists(Path.Combine(gitDir, "BISECT_LOG")))
|
|
{
|
|
ops.Add("BISECT IN PROGRESS");
|
|
}
|
|
}
|
|
|
|
return ops.Count > 0 ? string.Join(", ", ops) : "None";
|
|
}
|
|
|
|
private static string BuildPrompt(GitInfo info, string? errorMessage)
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
sb.AppendLine("Please diagnose this git issue and provide the fix:");
|
|
sb.AppendLine();
|
|
|
|
if (!string.IsNullOrEmpty(errorMessage))
|
|
{
|
|
sb.AppendLine("ERROR MESSAGE:");
|
|
sb.AppendLine(errorMessage);
|
|
sb.AppendLine();
|
|
}
|
|
|
|
sb.AppendLine("GIT STATUS:");
|
|
sb.AppendLine(info.Status);
|
|
sb.AppendLine();
|
|
|
|
sb.AppendLine("RECENT COMMITS:");
|
|
sb.AppendLine(info.Log);
|
|
sb.AppendLine();
|
|
|
|
sb.AppendLine("BRANCH INFO:");
|
|
sb.AppendLine(info.BranchInfo);
|
|
sb.AppendLine();
|
|
|
|
sb.AppendLine("REMOTE INFO:");
|
|
sb.AppendLine(info.RemoteInfo);
|
|
sb.AppendLine();
|
|
|
|
sb.AppendLine($"ONGOING OPERATIONS: {info.OngoingOps}");
|
|
|
|
if (!string.IsNullOrEmpty(info.Error))
|
|
{
|
|
sb.AppendLine();
|
|
sb.AppendLine($"ADDITIONAL ERROR: {info.Error}");
|
|
}
|
|
|
|
sb.AppendLine();
|
|
sb.AppendLine("Call ReturnGitDiagnosis with the problem, cause, fix command, any warnings, and risk level.");
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// AIConversation handles its own disposal
|
|
}
|
|
|
|
private class GitInfo
|
|
{
|
|
public string Status { get; set; } = string.Empty;
|
|
public string Log { get; set; } = string.Empty;
|
|
public string BranchInfo { get; set; } = string.Empty;
|
|
public string RemoteInfo { get; set; } = string.Empty;
|
|
public string OngoingOps { get; set; } = string.Empty;
|
|
public string Error { get; set; } = string.Empty;
|
|
}
|
|
}
|