* Code refactoring

This commit is contained in:
2026-01-23 12:54:51 +01:00
parent 2ecf448b74
commit 461b3e59fa
9 changed files with 230 additions and 158 deletions

View File

@@ -1,108 +1,95 @@
using System.Text;
using System.Text.Json; using System.Text.Json;
using Gitea.Net.Api; using Gitea.Net.Api;
using Gitea.Net.Client;
using Gitea.Net.Model; using Gitea.Net.Model;
using OpenAI; using PipelineAgent.ChangesChecker.Models;
using OpenAI.Chat; using Services.Gitea;
using Services.OpenAI;
using Services.Vault; using Services.Vault;
namespace PipelineAgent.ChangesChecker; namespace PipelineAgent.ChangesChecker;
public class ChangesCheckerAgent public class ChangesCheckerAgent(IOpenAiService openAiService, IGiteaService giteaService) : IChangesCheckerAgent
{ {
public async Task<int> CheckChangesAsync() public async Task<int> CheckChangesAsync(string repositoryPath)
{ {
try try
{ {
List<string> lastChangesFromGitea = await GetLastChangesFromGitea(); var vaultService = CreateVaultService();
var giteaApiToken = await vaultService.GetSecretAsync("api_keys/gitea", "gitea_api_token_write");
var userContent = JsonSerializer.Serialize(lastChangesFromGitea); var giteaConfiguration = GetGiteaConfiguration(repositoryPath, giteaApiToken);
var giteaClient = giteaService.CreateGiteaClient(giteaConfiguration);
var lastChangesFromGitea = await GetLastChangesFromGiteaAsync(giteaClient, giteaConfiguration);
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY"); if (lastChangesFromGitea.Count == 0)
if (string.IsNullOrWhiteSpace(apiKey))
{ {
Console.WriteLine("AIGate: OPENAI_API_KEY not set, blocking by default."); return 0;
}
var chatRequest = lastChangesFromGitea.Select(x => new ChatRequest(x.Key, x.Value));
var prompt = await File.ReadAllTextAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Prompts",
"ChangesChecker.txt"));
var decisionDoc = await openAiService.GetResponseFromChat(chatRequest, prompt);
if (decisionDoc == null)
{
Console.WriteLine("AIGate: no response from LLM, blocking by default.");
return 1; return 1;
} }
var client = new OpenAIClient(apiKey);
var systemPrompt = """
Jesteś Senior .NET Developerem z kilkunastoletnim stażem.
Na podstawie listy zawartości plików zmienionych w ostatnich commitach:
- zdekoduj zawartość pliku zapisaną w base64
- sprwadź kod pod kątem poprawności i potencjalnych błędów
- zasugeruj poprawki lub ulepszenia, jeśli to konieczne, tak, żeby kod był jak najbardziej optymalny i profesjonalny
- zwróć JEDEN obiekt JSON w formacie:
{
"file-content": "zdekodowana zawartość pliku",
"improvements": "wprowadzone poprawki w kodzie lub 'brak' w jezyku polskim",
"changed-file" "nowy plik z wprowadzonymi poprawkami lub 'brak'",
"decision": "approve" | "changes-requested"
}
Decyzja:
- "approve" gdy kod jest w porzadku i nie potrzebuje zadnych zmian.
- "changes-requested" gdy kod wymaga poprawek lub ulepszeń.
Nie dodawaj żadnego tekstu poza JSON.
""";
// var userContent = JsonSerializer.Serialize(failuresForModel);
//
var chat = client.GetChatClient("gpt-4.1-mini");
var response = await chat.CompleteChatAsync(new SystemChatMessage(systemPrompt),
new UserChatMessage($"Pliki ostatnio zmienione:\n{userContent}"));
var json = response.Value.Content[0].Text;
Console.WriteLine("AIGate LLM raw response:");
Console.WriteLine(json);
var decisionDoc = JsonDocument.Parse(json);
var decision = decisionDoc.RootElement.GetProperty("decision").GetString(); var decision = decisionDoc.RootElement.GetProperty("decision").GetString();
var improvements = decisionDoc.RootElement.GetProperty("improvements").GetString();
var fileContent = decisionDoc.RootElement.GetProperty("file-content").GetString();
var changedFile = decisionDoc.RootElement.GetProperty("changed-file").GetString();
Console.WriteLine($"AIGate decision: {decision}");
Console.WriteLine($"Improvements: {improvements}");
Console.WriteLine($"FileContent: {fileContent}");
Console.WriteLine($"ChangedFile: {changedFile}");
//
// return decision switch
// {
// "allow" => 0,
// "allow-with-warning" => 0,
// _ => 1
// };
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);
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 decision switch
{
"approve" => 0,
_ => 1
};
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"AIGate: error while reading TRX: {ex.Message}"); Console.WriteLine($"AIGate: while processing: {ex.Message}");
return 1; return 1;
} }
} }
private async Task<List<string>> GetLastChangesFromGitea() private async Task<Dictionary<string, string>> GetLastChangesFromGiteaAsync(RepositoryApi giteaClient,
GiteaConfiguration giteaConfiguration)
{ {
var lastChanges = new List<string>(); var lastChanges = new Dictionary<string, string>();
var lastCommit = await giteaService.GetLastCommitAsync(giteaClient, giteaConfiguration);
RepositoryApi repositoryApi = await CreateGiteaClient(); if (lastCommit == null || lastCommit.VarCommit.Message.Contains("LLM: Code review suggestions"))
{
return lastChanges;
}
var lastCommits = await repositoryApi.RepoGetAllCommitsAsync("FA", "FA", "master", limit: 2); foreach (CommitAffectedFiles commitAffectedFile in lastCommit.Files)
var commitAffectedFilesList = lastCommits.SelectMany(x => x.Files).ToList();
foreach (CommitAffectedFiles commitAffectedFile in commitAffectedFilesList)
{ {
var repoGetContentsAsync = var repoGetContentsAsync =
await repositoryApi.RepoGetContentsAsync("FA", "FA", commitAffectedFile.Filename); await giteaService.GetFileContentAsync(giteaClient, giteaConfiguration, commitAffectedFile.Filename);
if (!string.IsNullOrWhiteSpace(repoGetContentsAsync.Content)) if (!string.IsNullOrWhiteSpace(repoGetContentsAsync.Content) &&
!lastChanges.ContainsKey(commitAffectedFile.Filename))
{ {
lastChanges.Add(repoGetContentsAsync.Content); lastChanges.Add(commitAffectedFile.Filename, Base64Decode(repoGetContentsAsync.Content));
} }
} }
@@ -116,21 +103,64 @@ public class ChangesCheckerAgent
return new VaultService(vaultUrl, vaultToken); return new VaultService(vaultUrl, vaultToken);
} }
private async Task<RepositoryApi> CreateGiteaClient() private GiteaConfiguration GetGiteaConfiguration(string repositoryPath, string giteaApiToken)
{ {
VaultService vaultService = CreateVaultService(); string owner = repositoryPath.Split("/").SkipLast(1).Last();
string giteaApiToken = await vaultService.GetSecretAsync("api_keys/gitea", "gitea_api_token", "secret") ?? string repository = repositoryPath.Split("/").Last();
string.Empty; string branch = "master";
string host = "https://git.modwad.pl";
var config = new Configuration
return new GiteaConfiguration
{ {
BasePath = "https://git.modwad.pl/api/v1", Owner = owner,
ApiKey = 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>();
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
{ {
["token"] = giteaApiToken operation,
} path,
content = contentBase64
});
}
var payload = new
{
branch,
new_branch = newBranch,
message,
files
}; };
return new RepositoryApi(config); var jsonPayload = JsonSerializer.Serialize(payload);
return new StringContent(jsonPayload, Encoding.UTF8, "application/json");
} }
} }

View File

@@ -0,0 +1,6 @@
namespace PipelineAgent.ChangesChecker;
public interface IChangesCheckerAgent
{
Task<int> CheckChangesAsync(string repositoryPath);
}

View File

@@ -0,0 +1,3 @@
namespace PipelineAgent.ChangesChecker.Models;
public record ChatRequest(string FileName, string FileContent);

View File

@@ -9,8 +9,18 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Gitea.Net.API" Version="25.8.18" /> <PackageReference Include="Gitea.Net.API" Version="25.8.18" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.2.25163.2" />
<PackageReference Include="OpenAI" Version="2.8.0" /> <PackageReference Include="OpenAI" Version="2.8.0" />
<PackageReference Include="Services" Version="2.0.0-alpha.0" /> <PackageReference Include="Services" Version="2.0.0-alpha.0.134" />
</ItemGroup>
<ItemGroup>
<None Update="Prompts\TestsChecker.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Prompts\ChangesChecker.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,35 +1,38 @@
using System.Text.Json; using Microsoft.Extensions.DependencyInjection;
using System.Xml.Linq; using Microsoft.Extensions.Hosting;
using OpenAI;
using OpenAI.Chat;
using PipelineAgent.ChangesChecker; using PipelineAgent.ChangesChecker;
using PipelineAgent.TestsChecker; using PipelineAgent.TestsChecker;
using Services.Gitea;
using Services.OpenAI;
using Services.Vault;
IHost builder = Host.CreateDefaultBuilder(args)
.ConfigureServices((_, services) =>
{
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
var vaultService = new VaultService(vaultUrl, vaultToken);
services.AddSingleton<IVaultService>(vaultService);
services.AddScoped<IGiteaService, GiteaService>();
services.AddScoped<IOpenAiService, OpenAiService>();
services.AddScoped<ITestsCheckerAgent, TestsCheckerAgent>();
services.AddScoped<IChangesCheckerAgent, ChangesCheckerAgent>();
}).Build();
if (args.Length == 0) if (args.Length == 0)
{ {
Console.WriteLine("Usage: CiAgent <command> [options]"); Console.WriteLine("Usage: CiAgent <command> [options]");
Console.WriteLine("Commands: test-gate, container-gate, deploy-gate"); Console.WriteLine("Commands: test-gate, check-changes, deploy-gate");
return 1; return 1;
} }
var command = args[0]; return args[0] switch
var commandArgs = args.Skip(1).ToArray();
return command switch
{ {
"test-gate" => await RunTestGateAsync(commandArgs[0]), "test-gate" => await builder.Services.GetRequiredService<ITestsCheckerAgent>()
"check-changes" => await RunChangesGateAsync(), .CheckTestsAsync(args.ElementAtOrDefault(1) ?? string.Empty),
"check-changes" => await builder.Services.GetRequiredService<IChangesCheckerAgent>()
.CheckChangesAsync(args.ElementAtOrDefault(1) ?? string.Empty),
_ => 1 _ => 1
}; };
static async Task<int> RunTestGateAsync(string arg)
{
var testsCheckerAgent = new TestsCheckerAgent();
return await testsCheckerAgent.CheckTestsAsync(arg);
}
static async Task<int> RunChangesGateAsync()
{
var changesCheckerAgent = new ChangesCheckerAgent();
return await changesCheckerAgent.CheckChangesAsync();
}

View File

@@ -0,0 +1,38 @@
Jesteś Senior .NET Developerem z kilkunastoletnim stażem.
Programujesz w .NET 8 i C# 12, znasz najlepsze praktyki programistyczne oraz najnowoczesniejsza konwencje.
Na podstawie listy zawartości plików zmienionych w ostatnich commitach:
- sprwadź kod pod kątem poprawności i potencjalnych błędów
- zasugeruj poprawki lub ulepszenia, jeśli to konieczne, tak, żeby kod był jak najbardziej optymalny i profesjonalny
- zwróć JEDEN obiekt JSON w formacie:
{
decision: "approve" | "changes-requested",
changes:
{
branch = "master",
new_branch = "code-review_<losowy_ciag_znakow>",
message = "LLM: Code review suggestions plus to co zmieniono wypisane w liscie (*) po angielsku",
files = new[]
{
new
{
operation = "update",
path = nazwa pliku z requesta,
content = poprawiony plik
},
}
}
}
- tworz changes tylko wtedy jesli sugerujesz jakies zmiany w kodzie
- sprawdzaj, czy sugerowane zmiany nie wprowadzają nowych błędów lub problemów
- upewnij się, że sugerowane zmiany są zgodne z najlepszymi praktykami programistycznymi
- jesli klasa dziedziczy po interfejsie lub klasie bazowej, nie zmieniaj nazw metod lub ich sygnatur
- dostarczaj jasne i zwięzłe uzasadnienie dla każdej sugerowanej zmiany
- unikaj sugerowania zmian, które są zbędne lub nieistotne
- pamiętaj, że Twoim celem jest poprawa jakości kodu
- sprawdzaj wygenerowany json pod kątem poprawności składniowej i kompletności przed zwróceniem go użytkownikowi
- jesli nie sugerujesz żadnych zmian w kodzie, pole "changes" powinno być puste (null)
Decyzja:
- "approve" gdy kod jest w porzadku i nie potrzebuje zadnych zmian (pole changes jest null).
- "changes-requested" gdy kod wymaga poprawek lub ulepszeń.
Nie dodawaj żadnego tekstu poza JSON.

View File

@@ -0,0 +1,13 @@
Jesteś asystentem DevOps oceniającym wyniki testów .NET.
Na podstawie listy błędów:
- sklasyfikuj każdy błąd jako: CriticalBug, InfraOrConfig, FlakyTest
- zwróć JEDEN obiekt JSON w formacie:
{
"decision": "block" | "allow" | "allow-with-warning",
"reason": "krótkie wyjaśnienie po polsku"
}
Decyzja:
- "block" gdy jest choć jeden CriticalBug.
- "allow-with-warning" gdy są tylko InfraOrConfig lub FlakyTest, ale wygląda to na coś, co trzeba sprawdzić.
- "allow" gdy wszystko wskazuje na drobne lub znane flaky testy.
Nie dodawaj żadnego tekstu poza JSON.

View File

@@ -0,0 +1,6 @@
namespace PipelineAgent.TestsChecker;
public interface ITestsCheckerAgent
{
Task<int> CheckTestsAsync(string trxPath);
}

View File

@@ -1,11 +1,9 @@
using System.Text.Json;
using System.Xml.Linq; using System.Xml.Linq;
using OpenAI; using Services.OpenAI;
using OpenAI.Chat;
namespace PipelineAgent.TestsChecker; namespace PipelineAgent.TestsChecker;
public class TestsCheckerAgent public class TestsCheckerAgent(IOpenAiService openAiService) : ITestsCheckerAgent
{ {
public async Task<int> CheckTestsAsync(string trxPath) public async Task<int> CheckTestsAsync(string trxPath)
{ {
@@ -18,19 +16,11 @@ public class TestsCheckerAgent
try try
{ {
XDocument doc = XDocument.Load(trxPath); XDocument doc = XDocument.Load(trxPath);
// Standardowy namespace MSTest/TRX
XNamespace ns = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"; XNamespace ns = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010";
var failed = doc var failed = doc.Descendants(ns + "UnitTestResult").Where(x => (string?)x.Attribute("outcome") == "Failed")
.Descendants(ns + "UnitTestResult")
.Where(x => (string?)x.Attribute("outcome") == "Failed")
.ToList(); .ToList();
var total = doc.Descendants(ns + "UnitTestResult").Count();
var total = doc
.Descendants(ns + "UnitTestResult")
.Count();
var failuresForModel = failed.Select(f => new var failuresForModel = failed.Select(f => new
{ {
TestName = (string?)f.Attribute("testName") ?? "<no name>", TestName = (string?)f.Attribute("testName") ?? "<no name>",
@@ -41,48 +31,21 @@ public class TestsCheckerAgent
.Value .Value
}).ToList(); }).ToList();
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY"); var prompt = await File.ReadAllTextAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Prompts",
if (string.IsNullOrWhiteSpace(apiKey)) "TestsChecker.txt"));
var decisionDoc = await openAiService.GetResponseFromChat(failuresForModel, prompt);
if (decisionDoc == null)
{ {
Console.WriteLine("AIGate: OPENAI_API_KEY not set, blocking by default."); Console.WriteLine("AIGate: no response from LLM, blocking by default.");
return 1; return 1;
} }
var client = new OpenAIClient(apiKey);
var systemPrompt = """
Jesteś asystentem DevOps oceniającym wyniki testów .NET.
Na podstawie listy błędów:
- sklasyfikuj każdy błąd jako: CriticalBug, InfraOrConfig, FlakyTest
- zwróć JEDEN obiekt JSON w formacie:
{
"decision": "block" | "allow" | "allow-with-warning",
"reason": "krótkie wyjaśnienie po polsku"
}
Decyzja:
- "block" gdy jest choć jeden CriticalBug.
- "allow-with-warning" gdy tylko InfraOrConfig lub FlakyTest, ale wygląda to na coś, co trzeba sprawdzić.
- "allow" gdy wszystko wskazuje na drobne lub znane flaky testy.
Nie dodawaj żadnego tekstu poza JSON.
""";
var userContent = JsonSerializer.Serialize(failuresForModel);
var chat = client.GetChatClient("gpt-4.1-mini");
var response = await chat.CompleteChatAsync(new SystemChatMessage(systemPrompt),
new UserChatMessage($"Błędy testów:\n{userContent}"));
var json = response.Value.Content[0].Text;
Console.WriteLine("AIGate LLM raw response:");
Console.WriteLine(json);
var decisionDoc = JsonDocument.Parse(json);
var decision = decisionDoc.RootElement.GetProperty("decision").GetString(); var decision = decisionDoc.RootElement.GetProperty("decision").GetString();
var reason = decisionDoc.RootElement.GetProperty("reason").GetString(); var reason = decisionDoc.RootElement.GetProperty("reason").GetString();
Console.WriteLine($"Total: {total}, Failed: {failed.Count}");
Console.WriteLine($"AIGate decision: {decision}"); Console.WriteLine($"AIGate decision: {decision}");
Console.WriteLine($"Reason: {reason}"); Console.WriteLine($"Reason: {reason}");