* Code refactoring
This commit is contained in:
@@ -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("AI‑Gate: 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("AI‑Gate: 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("AI‑Gate 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($"AI‑Gate 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($"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 decision switch
|
||||||
|
{
|
||||||
|
"approve" => 0,
|
||||||
|
_ => 1
|
||||||
|
};
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"AI‑Gate: error while reading TRX: {ex.Message}");
|
Console.WriteLine($"AI‑Gate: 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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
PipelineAgent/ChangesChecker/IChangesCheckerAgent.cs
Normal file
6
PipelineAgent/ChangesChecker/IChangesCheckerAgent.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace PipelineAgent.ChangesChecker;
|
||||||
|
|
||||||
|
public interface IChangesCheckerAgent
|
||||||
|
{
|
||||||
|
Task<int> CheckChangesAsync(string repositoryPath);
|
||||||
|
}
|
||||||
3
PipelineAgent/ChangesChecker/Models/ChatRequest.cs
Normal file
3
PipelineAgent/ChangesChecker/Models/ChatRequest.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace PipelineAgent.ChangesChecker.Models;
|
||||||
|
|
||||||
|
public record ChatRequest(string FileName, string FileContent);
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
38
PipelineAgent/Prompts/ChangesChecker.txt
Normal file
38
PipelineAgent/Prompts/ChangesChecker.txt
Normal 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.
|
||||||
13
PipelineAgent/Prompts/TestsChecker.txt
Normal file
13
PipelineAgent/Prompts/TestsChecker.txt
Normal 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.
|
||||||
6
PipelineAgent/TestsChecker/ITestsCheckerAgent.cs
Normal file
6
PipelineAgent/TestsChecker/ITestsCheckerAgent.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace PipelineAgent.TestsChecker;
|
||||||
|
|
||||||
|
public interface ITestsCheckerAgent
|
||||||
|
{
|
||||||
|
Task<int> CheckTestsAsync(string trxPath);
|
||||||
|
}
|
||||||
@@ -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("AI‑Gate: OPENAI_API_KEY not set, blocking by default.");
|
Console.WriteLine("AI‑Gate: 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 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.
|
|
||||||
""";
|
|
||||||
|
|
||||||
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("AI‑Gate 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($"AI‑Gate decision: {decision}");
|
Console.WriteLine($"AI‑Gate decision: {decision}");
|
||||||
Console.WriteLine($"Reason: {reason}");
|
Console.WriteLine($"Reason: {reason}");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user