189 lines
6.9 KiB
C#
189 lines
6.9 KiB
C#
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("AI–Gate: no response from LLM, continuing by default.");
|
||
return 0;
|
||
}
|
||
|
||
if (!decisionDoc.RootElement.TryGetProperty("decision", out var decisionElement) ||
|
||
decisionElement.ValueKind == JsonValueKind.Null)
|
||
{
|
||
Console.WriteLine("AI–Gate: 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("AI–Gate: created PR with suggested changes.");
|
||
Console.WriteLine(response);
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine("AI–Gate: failed to create PR with suggested changes.");
|
||
Console.WriteLine(response);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
Console.WriteLine("AI–Gate: operation was canceled.");
|
||
return 0;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"AI–Gate: 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");
|
||
}
|
||
} |