Skip to content

Commit

Permalink
Feature/refactor chat script (#45)
Browse files Browse the repository at this point in the history
- Define "chat script" merging original definition and "chat definition"
- Define initial messages on chat script
- Refactor existing implementation of chat definition to chat script
  • Loading branch information
skarllot authored Jan 22, 2025
1 parent fb33f16 commit 4e11c9e
Show file tree
Hide file tree
Showing 35 changed files with 1,267 additions and 774 deletions.
4 changes: 2 additions & 2 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"sdk": {
"version": "9.0.101",
"rollForward": "latestMajor",
"rollForward": "disable",
"allowPrerelease": false
}
}
}
8 changes: 4 additions & 4 deletions src/FlowPair/Agent/Infrastructure/IAgentModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ namespace Raiqub.LlmTools.FlowPair.Agent.Infrastructure;
[Singleton(typeof(IDirectoryStructureMessageFactory), typeof(DirectoryStructureMessageFactory))]
[Singleton(typeof(IProjectFilesMessageFactory), typeof(ProjectFilesMessageFactory))]

// Chat definitions
[Singleton(typeof(IReviewChatDefinition), typeof(ReviewChatDefinition))]
[Singleton(typeof(ICreateUnitTestChatDefinition), typeof(CreateUnitTestChatDefinition))]
[Singleton(typeof(IUpdateUnitTestChatDefinition), typeof(UpdateUnitTestChatDefinition))]
// Chat scripts
[Singleton(typeof(IReviewChatScript), typeof(ReviewChatScript))]
[Singleton(typeof(ICreateUnitTestChatScript), typeof(CreateUnitTestChatScript))]
[Singleton(typeof(IUpdateUnitTestChatScript), typeof(UpdateUnitTestChatScript))]

// Operations
[Singleton(typeof(ILoginUseCase), typeof(LoginUseCase))]
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System.Collections.Immutable;
using Raiqub.LlmTools.FlowPair.Agent.Infrastructure;
using Raiqub.LlmTools.FlowPair.Agent.Operations.CreateUnitTest.v1;
using Raiqub.LlmTools.FlowPair.Agent.Services;
using Raiqub.LlmTools.FlowPair.Chats.Contracts.v1;
using Raiqub.LlmTools.FlowPair.Chats.Models;
using Raiqub.LlmTools.FlowPair.Chats.Services;
using Raiqub.LlmTools.FlowPair.Common;
using Raiqub.LlmTools.FlowPair.LocalFileSystem.Services;

namespace Raiqub.LlmTools.FlowPair.Agent.Operations.CreateUnitTest;

public interface ICreateUnitTestChatScript : IProcessableChatScript<CreateUnitTestRequest, CreateUnitTestResponse>;

public sealed class CreateUnitTestChatScript(
AgentJsonContext jsonContext,
IProjectFilesMessageFactory projectFilesMessageFactory,
IDirectoryStructureMessageFactory directoryStructureMessageFactory)
: ICreateUnitTestChatScript
{
private const string CodeResponseKey = "Markdown";
private const string JsonResponseKey = "JSON";

public string Name => "Create unit tests chat script";
public ImmutableArray<string> Extensions => KnownFileExtension.UnitTestable;

public string SystemInstruction =>
"""
You are an expert developer, your task is to create unit tests following the best practices.
You are given a set of project files, containing the filenames and their contents.
""";

public ImmutableList<Instruction> Instructions =>
[
Instruction.StepInstruction.Of(
"Create unit tests for the specified code"),
Instruction.StepInstruction.Of(
"Ensure the unit tests cover every possible execution path in the code"),
Instruction.StepInstruction.Of(
"Ensure the unit tests are sensitive to mutations in the source code. " +
"When mutation testing introduces small changes to the implementation (mutants), " +
"at least one test should fail. " +
"This verifies that the tests can detect potential bugs or behavioral changes."),
Instruction.StepInstruction.Of(
"Remove any redundant tests while maintaining full coverage"),
Instruction.CodeExtractInstruction.Of(
CodeResponseKey,
"Return the entire final version of the unit tests file content inside a code block (```)"),
Instruction.StepInstruction.Of(
"Where the new file for the unit tests should be located " +
"according to language and project standards?"),
Instruction.JsonConvertInstruction.Of(
JsonResponseKey,
"Reply the file path in a valid JSON format. The schema of the JSON object must be:",
FilePathResponse.Schema),
];

public ImmutableList<Message> GetInitialMessages(CreateUnitTestRequest input) =>
[
projectFilesMessageFactory.CreateWithProjectFilesContent(input.RootPath),
directoryStructureMessageFactory.CreateWithRepositoryStructure(input.RootPath),
..input.ExampleFileInfo is not null
?
[
new Message(
SenderRole.User,
$"""
Unit tests example:
```
{input.ExampleFileInfo.ReadAllText()}
```
"""),
]
: (ReadOnlySpan<Message>) [],
new Message(
SenderRole.User,
$"""
The source file to be tested is located at '{input.RootPath.GetRelativePath(input.FileInfo.FullName)}' and its content is:
```
{input.FileInfo.ReadAllText()}
```
"""),
];

public Result<object, string> Parse(string key, string input) => key switch
{
CodeResponseKey => MarkdownCodeExtractor
.TryExtractSingle(input)
.Select(static object (x) => x),
JsonResponseKey => JsonContentDeserializer
.TryDeserialize(input, jsonContext.FilePathResponse)
.Select(static object (x) => x),
_ => $"Unknown output key '{key}'",
};

public Option<CreateUnitTestResponse> CompileOutputs(ChatWorkspace chatWorkspace) =>
from filePath in OutputProcessor.GetFirst<FilePathResponse>(chatWorkspace, JsonResponseKey)
from codeSnippet in OutputProcessor.GetFirst<CodeSnippet>(chatWorkspace, CodeResponseKey)
select new CreateUnitTestResponse(filePath.FilePath, codeSnippet.Content);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using ConsoleAppFramework;
using Raiqub.LlmTools.FlowPair.Agent.Operations.CreateUnitTest.v1;
using Raiqub.LlmTools.FlowPair.Agent.Operations.Login;
using Raiqub.LlmTools.FlowPair.Agent.Services;
using Raiqub.LlmTools.FlowPair.Chats.Models;
using Raiqub.LlmTools.FlowPair.Chats.Services;
using Raiqub.LlmTools.FlowPair.Common;
Expand All @@ -15,10 +14,8 @@ namespace Raiqub.LlmTools.FlowPair.Agent.Operations.CreateUnitTest;
public sealed class CreateUnitTestCommand(
IAnsiConsole console,
IFileSystem fileSystem,
ICreateUnitTestChatDefinition chatDefinition,
ICreateUnitTestChatScript chatScript,
IWorkingDirectoryWalker workingDirectoryWalker,
IProjectFilesMessageFactory projectFilesMessageFactory,
IDirectoryStructureMessageFactory directoryStructureMessageFactory,
ILoginUseCase loginUseCase,
IChatService chatService)
{
Expand Down Expand Up @@ -53,9 +50,9 @@ public int Execute(
from session in loginUseCase.Execute(isBackground: true)
.UnwrapErrOr(0)
.Ensure(n => n == 0, 4)
let initialMessages = BuildInitialMessages(fileInfo, exampleFileInfo, rootPath).ToList()
let input = new CreateUnitTestRequest(fileInfo, exampleFileInfo, rootPath)
from response in chatService
.Run(console.Progress(), LlmModelType.Claude35Sonnet, chatDefinition, initialMessages)
.Run(input, console.Progress(), LlmModelType.Claude35Sonnet, chatScript)
.MapErr(HandleChatServiceError)
let testFile = CreateUnitTestFile(rootPath, response)
select 0)
Expand All @@ -74,36 +71,6 @@ private int HandleChatServiceError(string errorMessage)
return 5;
}

private IEnumerable<Message> BuildInitialMessages(
IFileInfo fileInfo,
IFileInfo? exampleFileInfo,
IDirectoryInfo rootPath)
{
yield return projectFilesMessageFactory.CreateWithProjectFilesContent(rootPath);
yield return directoryStructureMessageFactory.CreateWithRepositoryStructure(rootPath);

if (exampleFileInfo is not null)
{
yield return new Message(
SenderRole.User,
$"""
Unit tests example:
```
{exampleFileInfo.ReadAllText()}
```
""");
}

yield return new Message(
SenderRole.User,
$"""
The source file to be tested is located at '{rootPath.GetRelativePath(fileInfo.FullName)}' and its content is:
```
{fileInfo.ReadAllText()}
```
""");
}

private Unit CreateUnitTestFile(IDirectoryInfo rootPath, CreateUnitTestResponse response)
{
var normalizedFilePath = PathAnalyzer.Normalize(response.FilePath);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.IO.Abstractions;

namespace Raiqub.LlmTools.FlowPair.Agent.Operations.CreateUnitTest.v1;

public sealed record CreateUnitTestRequest(
IFileInfo FileInfo,
IFileInfo? ExampleFileInfo,
IDirectoryInfo RootPath);
37 changes: 17 additions & 20 deletions src/FlowPair/Agent/Operations/ReviewChanges/ReviewChangesCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using ConsoleAppFramework;
using Raiqub.LlmTools.FlowPair.Agent.Operations.Login;
using Raiqub.LlmTools.FlowPair.Agent.Operations.ReviewChanges.v1;
using Raiqub.LlmTools.FlowPair.Chats.Contracts.v1;
using Raiqub.LlmTools.FlowPair.Chats.Models;
using Raiqub.LlmTools.FlowPair.Chats.Services;
using Raiqub.LlmTools.FlowPair.Common;
Expand All @@ -13,8 +14,9 @@
namespace Raiqub.LlmTools.FlowPair.Agent.Operations.ReviewChanges;

public sealed class ReviewChangesCommand(
TimeProvider timeProvider,
IAnsiConsole console,
IReviewChatDefinition chatDefinition,
IReviewChatScript chatScript,
IGitGetChangesHandler getChangesHandler,
ILoginUseCase loginUseCase,
IChatService chatService,
Expand All @@ -41,12 +43,19 @@ from feedback in BuildFeedback(diff)

private Result<Unit, int> BuildFeedback(ImmutableList<FileChange> changes)
{
var feedback = changes
.GroupBy(c => ChatScript.FindChatScriptForFile([chatDefinition.ChatScript], c.Path))
.Where(g => g.Key.IsSome)
.Select(g => new { Script = g.Key.Unwrap(), Diff = g.AggregateToStringLines(c => c.Diff) })
.SelectMany(x => GetFeedback(x.Diff))
.Where(f => !string.IsNullOrWhiteSpace(f.Feedback))
var aggregatedChanges = changes
.Where(c => chatScript.CanHandleFile(c.Path))
.AggregateToStringLines(c => c.Diff);

var feedback = chatService
.Run(
new ReviewChangesRequest(aggregatedChanges),
console.Progress(),
LlmModelType.Claude35Sonnet,
chatScript)
.DoErr(error => console.MarkupLineInterpolated($"[red]Error:[/] {error}"))
.UnwrapOrElse(static () => [])
.Where(r => !string.IsNullOrWhiteSpace(r.Feedback))
.OrderByDescending(x => x.RiskScore).ThenBy(x => x.Path, StringComparer.OrdinalIgnoreCase)
.ToImmutableList();

Expand All @@ -56,23 +65,11 @@ private Result<Unit, int> BuildFeedback(ImmutableList<FileChange> changes)
{
tempFileWriter
.Write(
filename: $"{DateTime.UtcNow:yyyyMMddHHmmss}-feedback.html",
filename: $"{timeProvider.GetUtcNow():yyyyMMddHHmmss}-feedback.html",
content: new FeedbackHtmlTemplate(feedback).TransformText())
.LaunchFile(console);
}

return Unit();
}

private ImmutableList<ReviewerFeedbackResponse> GetFeedback(
string diff)
{
return chatService.Run(
console.Progress(),
LlmModelType.Claude35Sonnet,
chatDefinition,
[new Message(SenderRole.User, diff)])
.DoErr(error => console.MarkupLineInterpolated($"[red]Error:[/] {error}"))
.UnwrapOrElse(static () => []);
}
}
Loading

0 comments on commit 4e11c9e

Please sign in to comment.