Skip to content

Commit 5d8897f

Browse files
committed
Fixes to Blazor sample generation
1 parent 476e21d commit 5d8897f

File tree

19 files changed

+236
-198
lines changed

19 files changed

+236
-198
lines changed

samples/FizzBuzz.Generator.Basic/Program.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public async Task InvokeAsync()
2929
await csharp.CreateSdkGlobalAsync(
3030
new CreateSdkGlobalParameters(DotNetSdkVersion.Net8));
3131

32-
var mainProject = new DotNetProjectReference("FizzBuzz/FizzBuzz.csproj");
32+
var mainProject = new DotNetProjectReference("FizzBuzz/FizzBuzz.csproj", "FizzBuzz");
3333

3434
await commandLine.ExecuteCommandLineAsync(
3535
new ExecuteCommandLineParameters(

samples/FizzBuzz.Generator.OpenAI/Program.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public async Task InvokeAsync()
3434
await csharp.CreateSdkGlobalAsync(
3535
new CreateSdkGlobalParameters(DotNetSdkVersion.Net8));
3636

37-
var mainProject = new DotNetProjectReference("FizzBuzz/FizzBuzz.csproj");
37+
var mainProject = new DotNetProjectReference("FizzBuzz/FizzBuzz.csproj", "FizzBuzz");
3838

3939
await commandLine.ExecuteCommandLineAsync(
4040
new ExecuteCommandLineParameters(
@@ -43,11 +43,12 @@ await commandLine.ExecuteCommandLineAsync(
4343
await csharpGenerator.GenerateClassAsync(
4444
new GenerateClassParameters(
4545
mainProject,
46+
"",
4647
"Program",
4748
"A main method that implements the common fizz buzz app."));
4849

4950
await commandLine.ExecuteCommandLineAsync(
5051
new ExecuteCommandLineParameters(
51-
"dotnet run", mainProject.RelativeRoot, Interactive: true));
52+
"dotnet run", mainProject.RelativeRoot));
5253
}
5354
}

samples/TodoList.Blazor.Generator/Program.cs

+14-12
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public async Task InvokeAsync()
3434
await csharp.CreateSdkGlobalAsync(
3535
new CreateSdkGlobalParameters(DotNetSdkVersion.Net8));
3636

37-
var webProject = new DotNetProjectReference("TodoList.Web/TodoList.Web.csproj");
37+
var webProject = new DotNetProjectReference("TodoList.Web/TodoList.Web.csproj", "TodoList.Web");
3838

3939
await commandLine.ExecuteCommandLineAsync(
4040
new ExecuteCommandLineParameters(
@@ -45,7 +45,7 @@ await csharpGenerator.TransformClassAsync(
4545
webProject,
4646
"TodoList.Web/Program.cs",
4747
"""
48-
Use reflection to register any classes in the current assembly
48+
Use reflection to register any types that are classes in the current assembly
4949
that have a name that ends in `Service`, register them as scoped services.
5050
Register the default interface if possible. For example if TestService implements
5151
ITestService it should be registered as `services.AddScoped<ITestService, TestService>()`
@@ -55,7 +55,8 @@ Make sure the services are registered before the app container is built.
5555
var todoItem = await csharpGenerator.GenerateClassAsync(
5656
new GenerateClassParameters(
5757
webProject,
58-
"TodoList.Web.Models.TodoItem",
58+
"Models",
59+
"TodoItem",
5960
"""
6061
A model that represents a todo item. It should have the following properties:
6162
Id (guid)
@@ -67,10 +68,10 @@ Make sure the services are registered before the app container is built.
6768
var todoServiceInterface = await csharpGenerator.GenerateClassAsync(
6869
new GenerateClassParameters(
6970
webProject,
70-
"TodoList.Web.Service.ITodoService",
71+
"Services",
72+
"ITodoService",
7173
"""
72-
An interface for a service that provides CRUD actions for todo list items.
73-
Assume todo list items are of type TodoList.Web.Models.TodoItem.
74+
An interface for a service that provides CRUD actions for todo list items. Getting all items should return a list.
7475
""")
7576
{
7677
ContextMemoryItems = [todoItem]
@@ -79,16 +80,17 @@ Assume todo list items are of type TodoList.Web.Models.TodoItem.
7980
var todoService = await csharpGenerator.GenerateClassAsync(
8081
new GenerateClassParameters(
8182
webProject,
82-
"TodoList.Web.Service.TodoService",
83+
"Services",
84+
"TodoService",
8385
"""
84-
A service that implements ITodoService and provides CRUD actions for todo list items. Assume todo list items are of type
85-
TodoList.Web.Models.TodoItem. The items should be stored in a dictionary. Getting all items should return a list.
86+
A service that implements ITodoService and provides CRUD actions for todo list items.
87+
The items should be stored in a dictionary.
8688
""")
8789
{
8890
ContextMemoryItems = [todoItem, todoServiceInterface]
8991
});
9092

91-
await csharpGenerator.GenerateRazorComponentAsync(
93+
await csharpGenerator.GenerateBlazorComponentAsync(
9294
new GenerateRazorComponentParameters(
9395
webProject,
9496
"Components/Pages/TodoPage",
@@ -101,12 +103,12 @@ shows a listing of TodoList.Web.Models.TodoItem items.
101103
ContextMemoryItems = [todoItem, todoServiceInterface]
102104
});
103105

104-
await csharpGenerator.GenerateRazorComponentAsync(
106+
await csharpGenerator.GenerateBlazorComponentAsync(
105107
new GenerateRazorComponentParameters(
106108
webProject,
107109
"Components/Pages/Home",
108110
"""
109-
A Blazor page component
111+
A Blazor page component with the route "/"
110112
The page contents should be:
111113
A basic heading with a creative todo related title.
112114
A link to "Todo Items" at URL /todo.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Wolder.CommandLine.Actions.ExecuteCommandLine": "Warning"
6+
}
7+
}
8+
}

src/Wolder.CSharp.OpenAI/Actions/GenerateRazorComponent.cs renamed to src/Wolder.CSharp.OpenAI/Actions/GenerateBlazorComponent.cs

+43-25
Original file line numberDiff line numberDiff line change
@@ -16,80 +16,98 @@ public record GenerateRazorComponentParameters(
1616
Enumerable.Empty<FileMemoryItem>();
1717
}
1818

19-
public class GenerateRazorComponent(
19+
public class GenerateBlazorComponent(
2020
IAIAssistant assistant,
2121
ILogger<GenerateRazorComponentParameters> logger,
2222
DotNetProjectFactory projectFactory,
23+
CSharpActions csharp,
2324
ISourceFiles sourceFiles,
2425
GenerateRazorComponentParameters parameters)
2526
: IAction<GenerateRazorComponentParameters, FileMemoryItem>
2627
{
28+
private const string BlazorAssistantPrompt =
29+
"You are a C# Blazor component/page generator. Your output will be directly " +
30+
"written to a `.razor` file. Write terse but helpful comments to explain the code and its structure. " +
31+
"Add any usings for items used from the context. Ensure all comments use razor style comments @* comment *@. " +
32+
"It is crucial that the `@rendermode InteractiveServer` directive is included at the top of an interactive component/page " +
33+
"so that it works correctly in an interactive server scenario.";
34+
2735
public async Task<FileMemoryItem> InvokeAsync()
2836
{
29-
var (projectRef, className, behaviorPrompt) = parameters;
30-
var context = "";
37+
var (project, className, behaviorPrompt) = parameters;
38+
var tree = sourceFiles.GetDirectoryTree();
39+
var context = $$"""
40+
Directory tree of current project:
41+
{{tree}}
42+
""";
3143
if (parameters.ContextMemoryItems.Any())
3244
{
3345
context = "\nUsing the following for context:\n" +
3446
string.Join("\n", parameters.ContextMemoryItems
3547
.Select(i => $"File: {i.RelativePath}\n{i.Content}" ));
3648
}
3749
var response = await assistant.CompletePromptAsync($"""
38-
You are a C# Blazor component generator. Output only razor, your output will be directly written to a `.razor` file. Write terse but helpful comments to explain the code and its structure.
50+
{BlazorAssistantPrompt}
3951
40-
Based on the following context
4152
{context}
4253
43-
Create a razor component named `{className}` that adheres to the behavior described in `{behaviorPrompt}`. It is crucial that the `@rendermode InteractiveServer` directive is included for the component to function correctly in an interactive server scenario. Add any usings for items used from the context. ref Ensure all comments use razor style comments @* comment *@
54+
Create a Blazor component for the file `{className}.razor` that adheres to the behavior described here:
55+
{behaviorPrompt}
4456
""");
4557
var sanitized = Sanitize(response);
4658

4759
logger.LogInformation(sanitized);
4860

49-
var path = Path.Combine(projectRef.RelativeRoot, $"{className}.razor");
61+
var path = Path.Combine(project.RelativeRoot, $"{className}.razor");
5062

5163
await sourceFiles.WriteFileAsync(path, sanitized);
5264

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-
// }
65+
var result = await csharp.CompileProjectAsync(
66+
new(project));
67+
68+
if (result is CompilationResult.Failure failure)
69+
{
70+
var resolutionResult = await TryResolveFailedCompilationAsync(project, sanitized, failure, context);
71+
if (resolutionResult is CompilationResult.Failure)
72+
{
73+
throw new("Resolution failed");
74+
}
75+
}
6476

6577
return new FileMemoryItem(path, sanitized);
6678
}
6779

6880
private async Task<CompilationResult> TryResolveFailedCompilationAsync(
69-
DotNetProject project, string fileContent, CompilationResult lastResult, string context)
81+
DotNetProjectReference project, string fileContent, CompilationResult lastResult, string context)
7082
{
7183
var (projectRef, className, behaviorPrompt) = parameters;
7284
var maxAttempts = 2;
7385
for (int i = 0; i < maxAttempts; i++)
7486
{
7587
var messagesText = lastResult.Output.Errors;
7688
var response = await assistant.CompletePromptAsync($"""
77-
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.
89+
{BlazorAssistantPrompt}
7890
{context}
7991
80-
Given the following compilation diagnostic messages transform the following file to resolve the messages:
81-
{messagesText}
92+
The file `{className}.razor` was just generated by an automated assistant using this prompt:
93+
{behaviorPrompt}
8294
83-
File Content:
95+
```
8496
{fileContent}
97+
```
98+
99+
This file caused these compilation errors.
100+
{messagesText}
101+
102+
Create a new version of the file that resolves the errors.
85103
""");
86104

87105
var sanitized = Sanitize(response);
88106
logger.LogInformation(sanitized);
89-
var path = Path.Combine(projectRef.RelativeRoot, $"{className}.cs");
107+
var path = Path.Combine(projectRef.RelativeRoot, $"{className}.razor");
90108
await sourceFiles.WriteFileAsync(path, sanitized);
91109

92-
lastResult = await project.TryCompileAsync();
110+
lastResult = await csharp.CompileProjectAsync(new(project));
93111
if (lastResult is CompilationResult.Success)
94112
{
95113
break;

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

+55-27
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
using Wolder.Core.Files;
44
using Wolder.CSharp.Compilation;
55
using Microsoft.Extensions.Logging;
6+
using Wolder.CommandLine;
7+
using Wolder.CommandLine.Actions;
68
using Wolder.Core.Workspace;
9+
using Wolder.CSharp.Actions;
710

811
namespace Wolder.CSharp.OpenAI.Actions;
912

1013
public record GenerateClassParameters(
11-
DotNetProjectReference Project, string ClassFullName, string BehaviorPrompt)
14+
DotNetProjectReference Project, string Namespace, string ClassName, string BehaviorPrompt)
1215
{
1316
public IEnumerable<FileMemoryItem> ContextMemoryItems { get; init; } =
1417
Enumerable.Empty<FileMemoryItem>();
@@ -17,56 +20,69 @@ public record GenerateClassParameters(
1720
public class GenerateClass(
1821
IAIAssistant assistant,
1922
ILogger<GenerateClass> logger,
20-
DotNetProjectFactory projectFactory,
23+
CSharpActions csharp,
2124
ISourceFiles sourceFiles,
2225
GenerateClassParameters parameters)
2326
: IAction<GenerateClassParameters, FileMemoryItem>
2427
{
2528
public async Task<FileMemoryItem> InvokeAsync()
2629
{
27-
var (projectRef, className, behaviorPrompt) = parameters;
28-
var context = "";
30+
var (project, classNamespace, className, behaviorPrompt) = parameters;
31+
// Normalize the namespace to be relative to the project base namespace
32+
if (classNamespace.StartsWith(project.BaseNamespace))
33+
{
34+
classNamespace = classNamespace.Substring(project.BaseNamespace.Length);
35+
}
36+
37+
var tree = sourceFiles.GetDirectoryTree();
38+
var context = $$"""
39+
Directory Tree:
40+
{{tree}}
41+
""";
2942
if (parameters.ContextMemoryItems.Any())
3043
{
31-
context = "\nUsing the following for context:\n" +
44+
context = "\nThe items may also provide helpful context:\n" +
3245
string.Join("\n", parameters.ContextMemoryItems
3346
.Select(i => $"File: {i.RelativePath}\n{i.Content}" ));
3447
}
48+
49+
var namespaceEnd = string.IsNullOrEmpty(classNamespace)
50+
? ""
51+
: $".{classNamespace}";
3552
var response = await assistant.CompletePromptAsync($"""
3653
You are a C# code generator. Output only C#, your output will be directly written to a `.cs` file.
3754
Write terse but helpful explanatory comments.
3855
{context}
3956
40-
Create a class named `{className}` with the following behavior:
57+
Create a class named `{className}` with namespace `{project.BaseNamespace}{namespaceEnd}` with the following behavior:
4158
{behaviorPrompt}
4259
""");
43-
var sanitized = Sanitize(response);
44-
45-
logger.LogInformation(sanitized);
60+
61+
var classMemoryItem = await SanitizeAndWriteClassAsync(response);
4662

47-
var path = Path.Combine(projectRef.RelativeRoot, $"{className}.cs");
48-
49-
await sourceFiles.WriteFileAsync(path, sanitized);
50-
51-
var project = projectFactory.Create(projectRef);
52-
var result = await project.TryCompileAsync();
63+
var result = await csharp.CompileProjectAsync(new(project));
5364
if (result is CompilationResult.Failure failure)
5465
{
55-
var resolutionResult = await TryResolveFailedCompilationAsync(project, sanitized, failure, context);
66+
var (resolutionResult, fixedMemoryItem) = await TryResolveFailedCompilationAsync(project, classMemoryItem, failure, context);
5667
if (resolutionResult is CompilationResult.Failure)
5768
{
5869
throw new("Resolution failed");
5970
}
71+
else
72+
{
73+
return fixedMemoryItem ?? throw new NullReferenceException(nameof(fixedMemoryItem));
74+
}
6075
}
6176

62-
return new FileMemoryItem(path, sanitized);
77+
return classMemoryItem;
6378
}
6479

65-
private async Task<CompilationResult> TryResolveFailedCompilationAsync(
66-
IDotNetProject project, string fileContent, CompilationResult lastResult, string context)
80+
private async Task<(CompilationResult, FileMemoryItem?)> TryResolveFailedCompilationAsync(
81+
DotNetProjectReference project, FileMemoryItem lastFile, CompilationResult lastResult, string context)
6782
{
68-
var (projectRef, className, behaviorPrompt) = parameters;
83+
var (projectRef, classNamespace, className, behaviorPrompt) = parameters;
6984
var maxAttempts = 2;
85+
FileMemoryItem? classMemoryItem = null;
7086
for (int i = 0; i < maxAttempts; i++)
7187
{
7288
var diagnosticMessages = lastResult.Output.Errors;
@@ -79,21 +95,33 @@ You are a helpful assistant that writes C# code to complete any task specified b
7995
{messagesText}
8096
8197
File Content:
82-
{fileContent}
98+
{lastFile.Content}
8399
""");
84100

85-
var sanitized = Sanitize(response);
86-
logger.LogInformation(sanitized);
87-
var path = Path.Combine(projectRef.RelativeRoot, $"{className}.cs");
88-
await sourceFiles.WriteFileAsync(path, sanitized);
101+
classMemoryItem = await SanitizeAndWriteClassAsync(response);
89102

90-
lastResult = await project.TryCompileAsync();
103+
lastResult = await csharp.CompileProjectAsync(new(project));
91104
if (lastResult is CompilationResult.Success)
92105
{
93106
break;
94107
}
95108
}
96-
return lastResult;
109+
return (lastResult, classMemoryItem);
110+
}
111+
112+
private async Task<FileMemoryItem> SanitizeAndWriteClassAsync(string response)
113+
{
114+
var (project, classNamespace, className, behaviorPrompt) = parameters;
115+
var sanitized = Sanitize(response);
116+
117+
logger.LogInformation(sanitized);
118+
119+
var relativePath = classNamespace.Replace('.', Path.PathSeparator);
120+
var path = Path.Combine(project.RelativeRoot, relativePath, $"{className}.cs");
121+
122+
await sourceFiles.WriteFileAsync(path, sanitized);
123+
124+
return new FileMemoryItem(path, sanitized);
97125
}
98126

99127
private static string Sanitize(string input)

0 commit comments

Comments
 (0)