Skip to content

Commit 1e87372

Browse files
committed
WIP
1 parent c110966 commit 1e87372

File tree

15 files changed

+225
-254
lines changed

15 files changed

+225
-254
lines changed

samples/TodoList.Blazor.Generator/Program.cs

+82-60
Original file line numberDiff line numberDiff line change
@@ -19,67 +19,89 @@
1919

2020
var host = builder.Build();
2121

22-
var pipelineBuilder = host.Services.GetRequiredService<GeneratorPipelineBuilder>();
23-
var pipeline = pipelineBuilder.Build("TodoList.Blazor.Output");
22+
await host.Services.GetRequiredService<GeneratorWorkspaceBuilder>()
23+
.AddCommandLineActions()
24+
.AddCSharpGeneration()
25+
.InvokeAsync<GenerateTodoListApp>("TodoList.Blazor.Output");
2426

25-
var webProject = new DotNetProjectReference("TodoList.Web/TodoList.Web.csproj");
27+
class GenerateTodoListApp(
28+
CommandLineActions commandLine,
29+
CSharpActions csharp,
30+
CSharpGenerator csharpGenerator) : IVoidAction
31+
{
32+
public async Task InvokeAsync()
33+
{
34+
await csharp.CreateSdkGlobalAsync(
35+
new CreateSdkGlobalParameters(DotNetSdkVersion.Net8));
36+
37+
var webProject = new DotNetProjectReference("TodoList.Web/TodoList.Web.csproj");
38+
39+
await commandLine.ExecuteCommandLineAsync(
40+
new ExecuteCommandLineParameters(
41+
$"dotnet new blazor -o {webProject.Name} --interactivity server --empty"));
2642

27-
pipeline
28-
.AddStep(ctx => new CreateSdkGlobal(DotNetSdkVersion.Net8))
29-
.AddStep(ctx =>
30-
new RunCommand(
31-
$"dotnet new blazor -o {webProject.Name} --interactivity server --empty"))
32-
.AddStep(ctx =>
33-
new TransformClass(
34-
webProject,
35-
"TodoList.Web/Program.cs",
36-
"""
37-
Use reflection to register any classes in the current assembly
38-
that have a name that ends in `Service`, register them as scoped services.
39-
Register the default interface if possible. For example if TestService implements
40-
ITestService it should be registered as `services.AddScoped<ITestService, TestService>()`
41-
Make sure the services are registered before the app container is built.
42-
"""))
43-
.AddStep(ctx =>
44-
new GenerateClass(
45-
webProject,
46-
"TodoList.Web.Models.TodoItem",
47-
"""
48-
A model that represents a todo item. It should have the following properties:
49-
Id (guid)
50-
Title
51-
Completed
52-
Notes
53-
"""))
54-
.AddStep(ctx =>
55-
new GenerateClass(
56-
webProject,
57-
"TodoList.Web.Service.TodoService",
58-
"""
59-
A service that provides CRUD actions for todo list items. Assume todo list items are of type
60-
TodoList.Web.Models.TodoItem. The items should be stored in a dictionary. Getting all items should return a list.
61-
"""));
62-
// .AddStep(ctx =>
63-
// new GenerateClasses(
64-
// webProject,
65-
// "TodoList.Web",
66-
// """
67-
// A Blazor page component with route '/todo' that shows a listing of todo items and the supporting
68-
// service that holds the todo items in memory.
69-
// """))
70-
// .AddStep(ctx =>
71-
// new GenerateClasses(
72-
// webProject,
73-
// "TodoList.Web",
74-
// """
75-
// A single Blazor page component at Components/Pages/Home.razor.
76-
// Don't generate any other pages.
77-
// The page contents should be:
78-
// A basic heading.
79-
// A link to "ToDo Items" at URL /todo.
80-
// """))
81-
// .AddStep(ctx =>
82-
// new RunCommand("dotnet run", webProject.RelativeRoot, Interactive: true));
43+
await csharpGenerator.TransformClassAsync(
44+
new TransformClassParameters(
45+
webProject,
46+
"TodoList.Web/Program.cs",
47+
"""
48+
Use reflection to register any classes in the current assembly
49+
that have a name that ends in `Service`, register them as scoped services.
50+
Register the default interface if possible. For example if TestService implements
51+
ITestService it should be registered as `services.AddScoped<ITestService, TestService>()`
52+
Make sure the services are registered before the app container is built.
53+
"""));
8354

84-
await pipeline.RunAsync();
55+
var todoItem = await csharpGenerator.GenerateClassAsync(
56+
new GenerateClassParameters(
57+
webProject,
58+
"TodoList.Web.Models.TodoItem",
59+
"""
60+
A model that represents a todo item. It should have the following properties:
61+
Id (guid)
62+
Title
63+
Completed
64+
Notes
65+
"""));
66+
67+
var todoService = await csharpGenerator.GenerateClassAsync(
68+
new GenerateClassParameters(
69+
webProject,
70+
"TodoList.Web.Service.TodoService",
71+
"""
72+
A service that provides CRUD actions for todo list items. Assume todo list items are of type
73+
TodoList.Web.Models.TodoItem. The items should be stored in a dictionary. Getting all items should return a list.
74+
""")
75+
{
76+
ContextMemoryItems = [todoItem]
77+
});
78+
79+
await csharpGenerator.GenerateRazorComponentAsync(
80+
new GenerateRazorComponentParameters(
81+
webProject,
82+
"Components/Pages/TodoPage",
83+
"""
84+
A Blazor page component with route '/todo' that Injects TodoList.Web.Service.TodoService and
85+
shows a listing of TodoList.Web.Models.TodoItem items.
86+
""")
87+
{
88+
ContextMemoryItems = [todoItem, todoService]
89+
});
90+
91+
await csharpGenerator.GenerateRazorComponentAsync(
92+
new GenerateRazorComponentParameters(
93+
webProject,
94+
"Components/Pages/Home",
95+
"""
96+
A Blazor page component
97+
The page contents should be:
98+
A basic heading with a creative todo related title.
99+
A link to "Todo Items" at URL /todo.
100+
"""));
101+
102+
await commandLine.ExecuteCommandLineAsync(
103+
new ExecuteCommandLineParameters(
104+
"dotnet run", webProject.RelativeRoot, Interactive: true));
105+
}
106+
}
85107

src/Wolder.CSharp.OpenAI/Actions/GenerateClass.cs

+21-6
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,35 @@
77

88
namespace Wolder.CSharp.OpenAI.Actions;
99

10-
public record GenerateClassParameters(DotNetProjectReference project, string classFullName, string behaviorPrompt);
10+
public record GenerateClassParameters(
11+
DotNetProjectReference Project, string ClassFullName, string BehaviorPrompt)
12+
{
13+
public IEnumerable<FileMemoryItem> ContextMemoryItems { get; init; } =
14+
Enumerable.Empty<FileMemoryItem>();
15+
}
1116

12-
[GenerateTypedActionInvokeInterface<IGenerateClass>]
1317
public class GenerateClass(
1418
IAIAssistant assistant,
1519
ILogger<GenerateClass> logger,
1620
DotNetProjectFactory projectFactory,
1721
ISourceFiles sourceFiles,
1822
GenerateClassParameters parameters)
19-
: IVoidAction<GenerateClassParameters>
23+
: IAction<GenerateClassParameters, FileMemoryItem>
2024
{
21-
public async Task InvokeAsync()
25+
public async Task<FileMemoryItem> InvokeAsync()
2226
{
2327
var (projectRef, className, behaviorPrompt) = parameters;
28+
var context = "";
29+
if (parameters.ContextMemoryItems.Any())
30+
{
31+
context = "\nUsing the following for context:\n" +
32+
string.Join("\n", parameters.ContextMemoryItems
33+
.Select(i => $"File: {i.RelativePath}\n{i.Content}" ));
34+
}
2435
var response = await assistant.CompletePromptAsync($"""
2536
You are a C# code generator. Output only C#, your output will be directly written to a `.cs` file.
2637
Write terse but helpful explanatory comments.
38+
{context}
2739
2840
Create a class named `{className}` with the following behavior:
2941
{behaviorPrompt}
@@ -40,16 +52,18 @@ Write terse but helpful explanatory comments.
4052
var result = await project.TryCompileAsync();
4153
if (result is CompilationResult.Failure failure)
4254
{
43-
var resolutionResult = await TryResolveFailedCompilationAsync(parameters, project, sanitized, failure);
55+
var resolutionResult = await TryResolveFailedCompilationAsync(project, sanitized, failure, context);
4456
if (resolutionResult is CompilationResult.Failure)
4557
{
4658
throw new("Resolution failed");
4759
}
4860
}
61+
62+
return new FileMemoryItem(path, sanitized);
4963
}
5064

5165
private async Task<CompilationResult> TryResolveFailedCompilationAsync(
52-
GenerateClassParameters parameters, DotNetProject project, string fileContent, CompilationResult lastResult)
66+
DotNetProject project, string fileContent, CompilationResult lastResult, string context)
5367
{
5468
var (projectRef, className, behaviorPrompt) = parameters;
5569
var maxAttempts = 2;
@@ -59,6 +73,7 @@ private async Task<CompilationResult> TryResolveFailedCompilationAsync(
5973
var messagesText = string.Join(Environment.NewLine, diagnosticMessages);
6074
var response = await assistant.CompletePromptAsync($"""
6175
You are a helpful assistant that writes C# code to complete any task specified by me. Your output will be directly written to a file where it will be compiled as part of a larger C# project.
76+
{context}
6277
6378
Given the following compilation diagnostic messages transform the following file to resolve the messages:
6479
{messagesText}

src/Wolder.CSharp.OpenAI/Actions/GenerateClasses.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Text;
22
using System.Text.RegularExpressions;
3+
using Microsoft.CodeAnalysis;
34
using Wolder.Core.Assistants;
45
using Wolder.Core.Files;
56
using Wolder.CSharp.Compilation;
@@ -10,7 +11,6 @@ namespace Wolder.CSharp.OpenAI.Actions;
1011

1112
public record GenerateClassesParameters(DotNetProjectReference project, string filePath, string behaviorPrompt);
1213

13-
[GenerateTypedActionInvokeInterface<IGenerateClasses>]
1414
public class GenerateClasses(
1515
IAIAssistant assistant,
1616
ILogger<GenerateClasses> logger,
@@ -32,7 +32,7 @@ When you generate services you must also generate an interface i.e. TestService
3232
Generate `using {namespace};` for any referenced types. Usings are not automatically resolved.
3333
Assume the files will be added to a single dotnet 8.0 project. The base namespace of the
3434
project is `{{projectRef.Name}}`.
35-
Each file should always have a delimiter header like this:
35+
Each file should always have a delimited header like this:
3636
3737
// === START FILE: ProjectName/Namespace/Namespace/ClassName.cs
3838
File contents
@@ -48,24 +48,24 @@ File contents
4848
{{behaviorPrompt}}
4949
""");
5050

51-
await SplitAndSaveFilesAsync(response);
52-
5351
logger.LogInformation(response);
5452

55-
await SplitAndSaveFilesAsync(response);
53+
await SplitAndSaveFilesAsync(parameters, response);
5654

5755
var project = projectFactory.Create(projectRef);
5856
var compiles = await project.TryCompileAsync();
5957
if (compiles is CompilationResult.Failure)
6058
throw new("No compile");
6159
}
6260

63-
private async Task SplitAndSaveFilesAsync(string input)
61+
private async Task SplitAndSaveFilesAsync(GenerateClassesParameters parameters, string input)
6462
{
6563
var sanitized = Sanitize(input);
6664

6765
logger.LogInformation(sanitized);
6866

67+
var (projectRef, filePath, _) = parameters;
68+
var rootPath = Path.Combine(projectRef.RelativeRoot, filePath);
6969
using (StringReader reader = new StringReader(sanitized))
7070
{
7171
StringBuilder fileContent = new StringBuilder();
@@ -83,7 +83,7 @@ private async Task SplitAndSaveFilesAsync(string input)
8383
{
8484
if (currentFileName != null)
8585
{
86-
await sourceFiles.WriteFileAsync(currentFileName, fileContent.ToString());
86+
await sourceFiles.WriteFileAsync(Path.Combine(rootPath, currentFileName), fileContent.ToString());
8787
currentFileName = null;
8888
}
8989
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System.Text.RegularExpressions;
2+
using Wolder.Core.Assistants;
3+
using Wolder.Core.Files;
4+
using Wolder.CSharp.Compilation;
5+
using Microsoft.Extensions.Logging;
6+
using Wolder.Core.Workspace;
7+
8+
namespace Wolder.CSharp.OpenAI.Actions;
9+
10+
public record GenerateRazorComponentParameters(
11+
DotNetProjectReference project,
12+
string classFullName,
13+
string behaviorPrompt)
14+
{
15+
public IEnumerable<FileMemoryItem> ContextMemoryItems { get; set; } =
16+
Enumerable.Empty<FileMemoryItem>();
17+
}
18+
19+
public class GenerateRazorComponent(
20+
IAIAssistant assistant,
21+
ILogger<GenerateRazorComponentParameters> logger,
22+
DotNetProjectFactory projectFactory,
23+
ISourceFiles sourceFiles,
24+
GenerateRazorComponentParameters parameters)
25+
: IAction<GenerateRazorComponentParameters, FileMemoryItem>
26+
{
27+
public async Task<FileMemoryItem> InvokeAsync()
28+
{
29+
var (projectRef, className, behaviorPrompt) = parameters;
30+
var context = "";
31+
if (parameters.ContextMemoryItems.Any())
32+
{
33+
context = "\nUsing the following for context:\n" +
34+
string.Join("\n", parameters.ContextMemoryItems
35+
.Select(i => $"File: {i.RelativePath}\n{i.Content}" ));
36+
}
37+
var response = await assistant.CompletePromptAsync($"""
38+
You are a C# razor component generator. Output only C# and razor, your output will be directly written to a `.razor` file.
39+
Write terse but helpful comments.
40+
{context}
41+
42+
Create a razor component named `{className}` with the following behavior:
43+
{behaviorPrompt}
44+
""");
45+
var sanitized = Sanitize(response);
46+
47+
logger.LogInformation(sanitized);
48+
49+
var path = Path.Combine(projectRef.RelativeRoot, $"{className}.razor");
50+
51+
await sourceFiles.WriteFileAsync(path, sanitized);
52+
53+
var project = projectFactory.Create(projectRef);
54+
// TODO: Not working with razor for some reason
55+
// var result = await project.TryCompileAsync();
56+
// if (result is CompilationResult.Failure failure)
57+
// {
58+
// var resolutionResult = await TryResolveFailedCompilationAsync(project, sanitized, failure, context);
59+
// if (resolutionResult is CompilationResult.Failure)
60+
// {
61+
// throw new("Resolution failed");
62+
// }
63+
// }
64+
65+
return new FileMemoryItem(path, sanitized);
66+
}
67+
68+
private async Task<CompilationResult> TryResolveFailedCompilationAsync(
69+
DotNetProject project, string fileContent, CompilationResult lastResult, string context)
70+
{
71+
var (projectRef, className, behaviorPrompt) = parameters;
72+
var maxAttempts = 2;
73+
for (int i = 0; i < maxAttempts; i++)
74+
{
75+
var diagnosticMessages = lastResult.Diagnostics.Select(d => d.GetMessage());
76+
var messagesText = string.Join(Environment.NewLine, diagnosticMessages);
77+
var response = await assistant.CompletePromptAsync($"""
78+
You are a helpful assistant that writes C# razor component code to complete any task specified by me. Your output will be directly written to a file where it will be compiled as part of a larger C# project.
79+
{context}
80+
81+
Given the following compilation diagnostic messages transform the following file to resolve the messages:
82+
{messagesText}
83+
84+
File Content:
85+
{fileContent}
86+
""");
87+
88+
var sanitized = Sanitize(response);
89+
logger.LogInformation(sanitized);
90+
var path = Path.Combine(projectRef.RelativeRoot, $"{className}.cs");
91+
await sourceFiles.WriteFileAsync(path, sanitized);
92+
93+
lastResult = await project.TryCompileAsync();
94+
if (lastResult is CompilationResult.Success)
95+
{
96+
break;
97+
}
98+
}
99+
return lastResult;
100+
}
101+
102+
private static string Sanitize(string input)
103+
{
104+
string pattern = @"^\s*```\s*csharp|^\s*```|^\s*```\s*html";
105+
string result = Regex.Replace(input, pattern, "", RegexOptions.Multiline);
106+
107+
return result;
108+
}
109+
}

src/Wolder.CSharp.OpenAI/Actions/TransformClass.cs

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ namespace Wolder.CSharp.OpenAI.Actions;
1010

1111
public record TransformClassParameters(DotNetProjectReference project, string filePath, string behaviorPrompt);
1212

13-
[GenerateTypedActionInvokeInterface<ITransformClass>]
1413
public class TransformClass(
1514
IAIAssistant assistant,
1615
ILogger<TransformClass> logger,

0 commit comments

Comments
 (0)