Files
FA/PipelineAgent/ChangesChecker/ChangesCheckerAgent.cs

189 lines
6.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text;
using System.Text.Json;
using Gitea.Net.Api;
using Gitea.Net.Model;
using PipelineAgent.ChangesChecker.Models;
using Services.Gitea;
using Services.OpenAI;
using Services.Vault;
namespace PipelineAgent.ChangesChecker;
public class ChangesCheckerAgent(IOpenAiService openAiService, IVaultService vaultService, IGiteaService giteaService)
: IChangesCheckerAgent
{
public async Task<int> CheckChangesAsync(string repositoryPath)
{
try
{
var giteaApiToken = await vaultService.GetSecretAsync("api_keys/gitea", "gitea_api_token_write", "secret")
.ConfigureAwait(false);
var giteaConfiguration = GetGiteaConfiguration(repositoryPath, giteaApiToken);
var giteaClient = giteaService.CreateGiteaClient(giteaConfiguration);
var lastChangesFromGitea =
await GetLastChangesFromGiteaAsync(giteaClient, giteaConfiguration).ConfigureAwait(false);
if (lastChangesFromGitea.Count == 0)
{
return 0;
}
var chatRequest = lastChangesFromGitea.Select(x => new ChatRequest(x.Value.Item1, x.Key, x.Value.Item2));
var promptPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Prompts", "ChangesChecker.txt");
var prompt = await File.ReadAllTextAsync(promptPath).ConfigureAwait(false);
var decisionDoc = await openAiService.GetResponseFromChat(chatRequest, prompt).ConfigureAwait(false);
if (decisionDoc == null)
{
Console.WriteLine("AIGate: no response from LLM, continuing by default.");
return 0;
}
if (!decisionDoc.RootElement.TryGetProperty("decision", out var decisionElement) ||
decisionElement.ValueKind == JsonValueKind.Null)
{
Console.WriteLine("AIGate: decision property missing or null in LLM response, continuing by default.");
return 0;
}
if (!decisionDoc.RootElement.TryGetProperty("changes", out var changesElement) ||
changesElement.ValueKind == JsonValueKind.Null)
{
return 0;
}
var content = GetFormattedJsonRequest(changesElement);
var (status, response) = await giteaService.SendRequestAsync(giteaConfiguration, "contents", content)
.ConfigureAwait(false);
if (status)
{
Console.WriteLine("AIGate: created PR with suggested changes.");
Console.WriteLine(response);
}
else
{
Console.WriteLine("AIGate: failed to create PR with suggested changes.");
Console.WriteLine(response);
}
return 0;
}
catch (OperationCanceledException)
{
Console.WriteLine("AIGate: operation was canceled.");
return 0;
}
catch (Exception ex)
{
Console.WriteLine($"AIGate: while processing: {ex.Message}");
return 0;
}
}
private async Task<Dictionary<string, (string, string)>> GetLastChangesFromGiteaAsync(RepositoryApi giteaClient,
GiteaConfiguration giteaConfiguration)
{
var lastChanges = new Dictionary<string, (string, string)>(StringComparer.Ordinal);
var lastUpdatedBranch =
(await giteaService.GetAllBranchesAsync(giteaClient, giteaConfiguration).ConfigureAwait(false))
.Where(x => x.Name != "master" && x.Name.StartsWith("feature/")).OrderByDescending(x => x.Commit.Timestamp)
.FirstOrDefault();
if (lastUpdatedBranch == null ||
lastUpdatedBranch.Commit.Message.Contains("LLM: Code review suggestions", StringComparison.Ordinal))
{
return lastChanges;
}
var lastCommit = await giteaService
.GetCommitByIdAsync(giteaClient, giteaConfiguration, lastUpdatedBranch.Commit.Id).ConfigureAwait(false);
if (lastCommit == null)
{
return lastChanges;
}
foreach (CommitAffectedFiles commitAffectedFile in lastCommit.Files)
{
var fileContent = await giteaService
.GetFileContentAsync(giteaClient, giteaConfiguration, commitAffectedFile.Filename,
lastUpdatedBranch.Name).ConfigureAwait(false);
if (!string.IsNullOrEmpty(fileContent.Content) && !lastChanges.ContainsKey(commitAffectedFile.Filename))
{
lastChanges.Add(commitAffectedFile.Filename, (lastUpdatedBranch.Name, Base64Decode(fileContent.Content)));
}
}
return lastChanges;
}
private GiteaConfiguration GetGiteaConfiguration(string repositoryPath, string giteaApiToken)
{
ReadOnlySpan<char> repoSpan = repositoryPath.AsSpan();
int lastSlash = repoSpan.LastIndexOf('/');
int secondLastSlash = lastSlash > 0 ? repoSpan.Slice(0, lastSlash).LastIndexOf('/') : -1;
string owner = secondLastSlash >= 0
? repoSpan.Slice(secondLastSlash + 1, lastSlash - secondLastSlash - 1).ToString()
: string.Empty;
string repository = lastSlash >= 0 ? repoSpan.Slice(lastSlash + 1).ToString() : repositoryPath;
const string branch = "master";
const string host = "https://git.modwad.pl";
return new GiteaConfiguration
{
Owner = owner,
Repository = repository,
Branch = branch,
ApiToken = giteaApiToken,
Host = host
};
}
private string Base64Decode(string base64EncodedData)
{
byte[] bytes = Convert.FromBase64String(base64EncodedData);
return Encoding.UTF8.GetString(bytes);
}
private StringContent GetFormattedJsonRequest(JsonElement json)
{
var branch = json.GetProperty("branch").GetString();
var newBranch = json.GetProperty("new_branch").GetString();
var message = json.GetProperty("message").GetString();
var filesJson = json.GetProperty("files");
var files = new List<object>(filesJson.GetArrayLength());
foreach (var fileEl in filesJson.EnumerateArray())
{
var operation = fileEl.GetProperty("operation").GetString();
var path = fileEl.GetProperty("path").GetString();
var contentPlain = fileEl.GetProperty("content").GetString() ?? string.Empty;
var contentBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(contentPlain));
files.Add(new
{
operation,
path,
content = contentBase64
});
}
var payload = new
{
branch,
new_branch = newBranch,
message,
files
};
var jsonPayload = JsonSerializer.Serialize(payload);
return new StringContent(jsonPayload, Encoding.UTF8, "application/json");
}
}