diff --git a/.gitignore b/.gitignore index e152adc9d..0da63ea87 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ # Mono auto generated files mono_crash.* +output + # Build results [Dd]ebug/ [Dd]ebugPublic/ diff --git a/cli/Models/DevLeadPlanResponse.cs b/cli/Models/DevLeadPlanResponse.cs index 52fcc6a73..a68eacf91 100644 --- a/cli/Models/DevLeadPlanResponse.cs +++ b/cli/Models/DevLeadPlanResponse.cs @@ -2,24 +2,18 @@ using System.Text.Json.Serialization; public class Subtask { - [JsonPropertyName("subtask")] - public string Name { get; set; } - [JsonPropertyName("LLM_prompt")] - public string LLMPrompt { get; set; } + public string subtask { get; set; } + public string LLM_prompt { get; set; } } public class Step { - [JsonPropertyName("step")] - public string Name { get; set; } - [JsonPropertyName("description")] - public string Description { get; set; } - [JsonPropertyName("subtasks")] - public List Subtasks { get; set; } + public string description { get; set; } + public string step { get; set; } + public List subtasks { get; set; } } public class DevLeadPlanResponse { - [JsonPropertyName("steps")] - public List Steps { get; set; } + public List steps { get; set; } } \ No newline at end of file diff --git a/cli/Program.cs b/cli/Program.cs index 7d8b89e1c..66fd3ff33 100644 --- a/cli/Program.cs +++ b/cli/Program.cs @@ -1,9 +1,5 @@ -using System; using System.CommandLine; -using System.IO; -using System.Net.Http; using System.Text.Json; -using System.Threading.Tasks; class Program { @@ -16,50 +12,88 @@ class Program var rootCommand = new RootCommand("CLI tool for the AI Dev team"); rootCommand.AddGlobalOption(fileOption); + var doCommand = new Command("do", "Doers :) "); + var doItCommand = new Command("it", "Do it!"); + doItCommand.SetHandler(async (file) => await ChainFunctions(file.FullName), fileOption); + doCommand.AddCommand(doItCommand); + var pmCommand = new Command("pm", "Commands for the PM team"); var pmReadmeCommand = new Command("readme", "Produce a Readme for a given input"); - pmReadmeCommand.SetHandler(async (file) => await CallFunction(nameof(PM), PM.Readme , file.FullName), fileOption); + pmReadmeCommand.SetHandler(async (file) => await CallWithFile(nameof(PM), PM.Readme , file.FullName), fileOption); var pmBootstrapCommand = new Command("bootstrap", "Bootstrap a project for a given input"); - pmBootstrapCommand.SetHandler(async (file) => await CallFunction(nameof(PM), PM.BootstrapProject, file.FullName), fileOption); + pmBootstrapCommand.SetHandler(async (file) => await CallWithFile(nameof(PM), PM.BootstrapProject, file.FullName), fileOption); pmCommand.AddCommand(pmReadmeCommand); pmCommand.AddCommand(pmBootstrapCommand); var devleadCommand = new Command("devlead", "Commands for the Dev Lead team"); var devleadPlanCommand = new Command("plan", "Plan the work for a given input"); - devleadPlanCommand.SetHandler(async (file) => await CallFunction(nameof(DevLead), DevLead.Plan, file.FullName), fileOption); + devleadPlanCommand.SetHandler(async (file) => await CallWithFile(nameof(DevLead), DevLead.Plan, file.FullName), fileOption); devleadCommand.AddCommand(devleadPlanCommand); var devCommand = new Command("dev", "Commands for the Dev team"); var devPlanCommand = new Command("plan", "Implement the module for a given input"); - devPlanCommand.SetHandler(async (file) => await CallFunction(nameof(Developer), Developer.Implement, file.FullName), fileOption); + devPlanCommand.SetHandler(async (file) => await CallWithFile(nameof(Developer), Developer.Implement, file.FullName), fileOption); devCommand.AddCommand(devPlanCommand); rootCommand.AddCommand(pmCommand); rootCommand.AddCommand(devleadCommand); rootCommand.AddCommand(devCommand); + rootCommand.AddCommand(doCommand); await rootCommand.InvokeAsync(args); } - public static async Task CallFunction(string skillName, string functionName, string file) + public static async Task ChainFunctions(string file) { - if (!File.Exists(file)) - { - Console.WriteLine($"File not found: {file}"); - return default; - } + var sandboxSkill = new SandboxSkill(); + var outputPath = Directory.CreateDirectory("output"); + var readme = await CallWithFile(nameof(PM), PM.Readme , file); + await SaveToFile(Path.Combine(outputPath.FullName, "README.md"), readme); + + var script = await CallWithFile(nameof(PM), PM.BootstrapProject, file); + await sandboxSkill.RunInDotnetAlpineAsync(script); + await SaveToFile(Path.Combine(outputPath.FullName, "bootstrap.sh"), script); + + var plan = await CallWithFile(nameof(DevLead), DevLead.Plan, file); + await SaveToFile(Path.Combine(outputPath.FullName, "plan.json"), JsonSerializer.Serialize(plan)); + + var implementationTasks = plan.steps.SelectMany( + (step) => step.subtasks.Select( + async (subtask) => { + var implementationResult = await CallFunction(nameof(Developer), Developer.Implement, subtask.LLM_prompt); + await sandboxSkill.RunInDotnetAlpineAsync(implementationResult); + await SaveToFile(Path.Combine(outputPath.FullName, $"{step.step}-{subtask.subtask}.sh"), implementationResult); + return implementationResult; })); + await Task.WhenAll(implementationTasks); + } + + public static async Task SaveToFile(string filePath, string content) + { + await File.WriteAllTextAsync(filePath, content); + } + + public static async Task CallWithFile(string skillName, string functionName, string filePath) + { + if(!File.Exists(filePath)) + throw new FileNotFoundException($"File not found: {filePath}", filePath); + var input = File.ReadAllText(filePath); + return await CallFunction(skillName, functionName, input); + } + + public static async Task CallFunction(string skillName, string functionName, string input) + { var variables = new[] { - new { key = "input", value = File.ReadAllText(file) } + new { key = "input", value = input } }; var requestBody = new { variables }; var requestBodyJson = JsonSerializer.Serialize(requestBody); - Console.WriteLine($"Calling skill '{skillName}' function '{functionName}' with file '{file}'"); + Console.WriteLine($"Calling skill '{skillName}' function '{functionName}' with input '{input}'"); Console.WriteLine(requestBodyJson); using var httpClient = new HttpClient(); diff --git a/cli/SandboxSkill.cs b/cli/SandboxSkill.cs new file mode 100644 index 000000000..0cc9f310d --- /dev/null +++ b/cli/SandboxSkill.cs @@ -0,0 +1,44 @@ +using DotNet.Testcontainers.Builders; +using Microsoft.SemanticKernel.SkillDefinition; + +public class SandboxSkill +{ + [SKFunction("Run a script in Alpine sandbox")] + [SKFunctionInput(Description = "The script to be executed")] + [SKFunctionName("RunInAlpine")] + public async Task RunInAlpineAsync(string input) + { + return await RunInContainer(input, "alpine"); + } + + [SKFunction("Run a script in dotnet alpine sandbox")] + [SKFunctionInput(Description = "The script to be executed")] + [SKFunctionName("RunInDotnetAlpine")] + public async Task RunInDotnetAlpineAsync(string input) + { + return await RunInContainer(input, "mcr.microsoft.com/dotnet/sdk:7.0"); + } + + private async Task RunInContainer(string input, string image) + { + var tempScriptFile = $"{Guid.NewGuid().ToString()}.sh"; + var tempScriptPath = $"./output/{tempScriptFile}"; + await File.WriteAllTextAsync(tempScriptPath, input); + Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(),"output", "src")); + var dotnetContainer = new ContainerBuilder() + .WithName(Guid.NewGuid().ToString("D")) + .WithImage(image) + .WithBindMount(Path.Combine(Directory.GetCurrentDirectory(),"output", "src"), "/src") + .WithBindMount(Path.Combine(Directory.GetCurrentDirectory(), tempScriptPath), $"/src/{tempScriptFile}") + .WithWorkingDirectory("/src") + .WithCommand("sh", tempScriptFile) + .Build(); + + await dotnetContainer.StartAsync() + .ConfigureAwait(false); + // Cleanup + File.Delete(tempScriptPath); + File.Delete(Path.Combine(Directory.GetCurrentDirectory(), "output", "src", tempScriptFile)); + return ""; + } +} \ No newline at end of file diff --git a/cli/cli.csproj b/cli/cli.csproj index 1b19e9dce..a36d72b07 100644 --- a/cli/cli.csproj +++ b/cli/cli.csproj @@ -10,6 +10,8 @@ + + diff --git a/cli/util/ToDoListSamplePrompt.txt b/cli/util/ToDoListSamplePrompt.txt index 6815c3837..e8fe720b8 100644 --- a/cli/util/ToDoListSamplePrompt.txt +++ b/cli/util/ToDoListSamplePrompt.txt @@ -1,4 +1,5 @@ I'd like to build a typical Todo List Application: a simple productivity tool that allows users to create, manage, and track tasks or to-do items. Key features of the Todo List application include the ability to add, edit, and delete tasks, set due dates and reminders, categorize tasks by project or priority, and mark tasks as complete. The Todo List applications also offer collaboration features, such as sharing tasks with others or assigning tasks to team members. -Additionally, the Todo List application will offer offer mobile and web-based interfaces, allowing users to access their tasks from anywhere. \ No newline at end of file +Additionally, the Todo List application will offer offer mobile and web-based interfaces, allowing users to access their tasks from anywhere. +Use C# as the language. \ No newline at end of file diff --git a/sk-azfunc-server/skills/Developer/Implement/skprompt.txt b/sk-azfunc-server/skills/Developer/Implement/skprompt.txt index a2ea0bf9b..c396228a8 100644 --- a/sk-azfunc-server/skills/Developer/Implement/skprompt.txt +++ b/sk-azfunc-server/skills/Developer/Implement/skprompt.txt @@ -1,5 +1,6 @@ You are a Developer for an application. -Please output the code or script required to accomplish the task assigned to you below. +Please output the code required to accomplish the task assigned to you below and wrap it in a bash script that creates the files. +Do not use any IDE commands and do not build and run the code. Make specific choices about implementation. Do not offer a range of options. Use comments in the code to describe the intent. Do not include other text other than code and code comments. Input: {{$input}} \ No newline at end of file