Merge pull request #2 from microsoft/refactor_skills

Skills refactor, CLI calls local kernel
This commit is contained in:
Ryan Sweet
2023-06-09 12:58:38 -07:00
committed by GitHub
21 changed files with 298 additions and 142 deletions

View File

@@ -1,7 +0,0 @@
using System.Text.Json.Serialization;
public class SkillsResponse
{
[JsonPropertyName("response")]
public string? Response { get; set; }
}

View File

@@ -1,10 +1,32 @@
using System.CommandLine;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using skills;
class Program
{
static async Task Main(string[] args)
{
var kernelSettings = KernelSettings.LoadSettings();
var kernelConfig = new KernelConfig();
kernelConfig.AddCompletionBackend(kernelSettings);
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
builder
.SetMinimumLevel(kernelSettings.LogLevel ?? LogLevel.Warning)
.AddConsole()
.AddDebug();
});
var kernel = new KernelBuilder()
.WithLogger(loggerFactory.CreateLogger<IKernel>())
.WithConfiguration(kernelConfig).Build();
var fileOption = new Option<FileInfo?>(
name: "--file",
description: "The file used for input to the skill function");
@@ -14,27 +36,27 @@ class Program
var doCommand = new Command("do", "Doers :) ");
var doItCommand = new Command("it", "Do it!");
doItCommand.SetHandler(async (file) => await ChainFunctions(file.FullName), fileOption);
doItCommand.SetHandler(async (file) => await ChainFunctions(file.FullName, kernel), 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 CallWithFile<string>(nameof(PM), PM.Readme , file.FullName), fileOption);
pmReadmeCommand.SetHandler(async (file) => await CallWithFile<string>(nameof(PM), PM.Readme , file.FullName, kernel), fileOption);
var pmBootstrapCommand = new Command("bootstrap", "Bootstrap a project for a given input");
pmBootstrapCommand.SetHandler(async (file) => await CallWithFile<string>(nameof(PM), PM.BootstrapProject, file.FullName), fileOption);
pmBootstrapCommand.SetHandler(async (file) => await CallWithFile<string>(nameof(PM), PM.BootstrapProject, file.FullName, kernel), 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 CallWithFile<DevLeadPlanResponse>(nameof(DevLead), DevLead.Plan, file.FullName), fileOption);
devleadPlanCommand.SetHandler(async (file) => await CallWithFile<DevLeadPlanResponse>(nameof(DevLead), DevLead.Plan, file.FullName, kernel), 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 CallWithFile<string>(nameof(Developer), Developer.Implement, file.FullName), fileOption);
devPlanCommand.SetHandler(async (file) => await CallWithFile<string>(nameof(Developer), Developer.Implement, file.FullName, kernel), fileOption);
devCommand.AddCommand(devPlanCommand);
rootCommand.AddCommand(pmCommand);
@@ -45,25 +67,25 @@ class Program
await rootCommand.InvokeAsync(args);
}
public static async Task ChainFunctions(string file)
public static async Task ChainFunctions(string file, IKernel kernel)
{
var sandboxSkill = new SandboxSkill();
var outputPath = Directory.CreateDirectory("output");
var readme = await CallWithFile<string>(nameof(PM), PM.Readme , file);
var readme = await CallWithFile<string>(nameof(PM), PM.Readme , file, kernel);
await SaveToFile(Path.Combine(outputPath.FullName, "README.md"), readme);
var script = await CallWithFile<string>(nameof(PM), PM.BootstrapProject, file);
var script = await CallWithFile<string>(nameof(PM), PM.BootstrapProject, file, kernel);
await sandboxSkill.RunInDotnetAlpineAsync(script);
await SaveToFile(Path.Combine(outputPath.FullName, "bootstrap.sh"), script);
var plan = await CallWithFile<DevLeadPlanResponse>(nameof(DevLead), DevLead.Plan, file);
var plan = await CallWithFile<DevLeadPlanResponse>(nameof(DevLead), DevLead.Plan, file, kernel);
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<string>(nameof(Developer), Developer.Implement, subtask.LLM_prompt);
var implementationResult = await CallFunction<string>(nameof(Developer), Developer.Implement, subtask.LLM_prompt, kernel);
await sandboxSkill.RunInDotnetAlpineAsync(implementationResult);
await SaveToFile(Path.Combine(outputPath.FullName, $"{step.step}-{subtask.subtask}.sh"), implementationResult);
return implementationResult; }));
@@ -75,43 +97,28 @@ class Program
await File.WriteAllTextAsync(filePath, content);
}
public static async Task<T> CallWithFile<T>(string skillName, string functionName, string filePath)
public static async Task<T> CallWithFile<T>(string skillName, string functionName, string filePath, IKernel kernel)
{
if(!File.Exists(filePath))
throw new FileNotFoundException($"File not found: {filePath}", filePath);
var input = File.ReadAllText(filePath);
return await CallFunction<T>(skillName, functionName, input);
return await CallFunction<T>(skillName, functionName, input, kernel);
}
public static async Task<T> CallFunction<T>(string skillName, string functionName, string input)
public static async Task<T> CallFunction<T>(string skillName, string functionName, string input, IKernel kernel)
{
var variables = new[]
{
new { key = "input", value = input }
};
var requestBody = new { variables };
var requestBodyJson = JsonSerializer.Serialize(requestBody);
Console.WriteLine($"Calling skill '{skillName}' function '{functionName}' with input '{input}'");
Console.WriteLine(requestBodyJson);
var skillConfig = SemanticFunctionConfig.ForSkillAndFunction(skillName, functionName);
var function = kernel.CreateSemanticFunction(skillConfig.PromptTemplate, skillConfig.Name, skillConfig.SkillName,
skillConfig.Description, skillConfig.MaxTokens, skillConfig.Temperature,
skillConfig.TopP, skillConfig.PPenalty, skillConfig.FPenalty);
using var httpClient = new HttpClient();
var apiUrl = $"http://localhost:7071/api/skills/{skillName}/functions/{functionName}";
var response = await httpClient.PostAsync(apiUrl, new StringContent(requestBodyJson));
var context = new ContextVariables();
context.Set("input", input);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"Error: {response.StatusCode} - {response.ReasonPhrase}");
return default;
}
var responseJson = await response.Content.ReadAsStringAsync();
var skillResponse = JsonSerializer.Deserialize<SkillsResponse>(responseJson);
var result = typeof(T) != typeof(string) ? JsonSerializer.Deserialize<T>(skillResponse.Response) : (T)(object)skillResponse.Response;
Console.WriteLine(responseJson);
var answer = await kernel.RunAsync(context, function).ConfigureAwait(false);
var result = typeof(T) != typeof(string) ? JsonSerializer.Deserialize<T>(answer.ToString()) : (T)(object)answer.ToString();
Console.WriteLine(answer);
return result;
}
}

View File

@@ -9,9 +9,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="Microsoft.SemanticKernel" Version="0.14.547.1-preview" />
<PackageReference Include="Testcontainers" Version="3.2.0" />
<ProjectReference Include="..\skills\skills.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,27 @@
using Microsoft.SemanticKernel;
internal static class KernelConfigExtensions
{
/// <summary>
/// Adds a text completion service to the list. It can be either an OpenAI or Azure OpenAI backend service.
/// </summary>
/// <param name="kernelConfig"></param>
/// <param name="kernelSettings"></param>
/// <exception cref="ArgumentException"></exception>
internal static void AddCompletionBackend(this KernelConfig kernelConfig, KernelSettings kernelSettings)
{
switch (kernelSettings.ServiceType.ToUpperInvariant())
{
case KernelSettings.AzureOpenAI:
kernelConfig.AddAzureChatCompletionService(kernelSettings.DeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey);
break;
case KernelSettings.OpenAI:
kernelConfig.AddOpenAITextCompletionService(modelId: kernelSettings.DeploymentOrModelId, apiKey: kernelSettings.ApiKey, orgId: kernelSettings.OrgId, serviceId: kernelSettings.ServiceId);
break;
default:
throw new ArgumentException($"Invalid service type value: {kernelSettings.ServiceType}");
}
}
}

View File

@@ -0,0 +1,89 @@
using System.Text.Json.Serialization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
internal class KernelSettings
{
public const string DefaultConfigFile = "config/appsettings.json";
public const string OpenAI = "OPENAI";
public const string AzureOpenAI = "AZUREOPENAI";
[JsonPropertyName("serviceType")]
public string ServiceType { get; set; } = string.Empty;
[JsonPropertyName("serviceId")]
public string ServiceId { get; set; } = string.Empty;
[JsonPropertyName("deploymentOrModelId")]
public string DeploymentOrModelId { get; set; } = string.Empty;
[JsonPropertyName("endpoint")]
public string Endpoint { get; set; } = string.Empty;
[JsonPropertyName("apiKey")]
public string ApiKey { get; set; } = string.Empty;
[JsonPropertyName("orgId")]
public string OrgId { get; set; } = string.Empty;
[JsonPropertyName("logLevel")]
public LogLevel? LogLevel { get; set; }
/// <summary>
/// Load the kernel settings from settings.json if the file exists and if not attempt to use user secrets.
/// </summary>
internal static KernelSettings LoadSettings()
{
try
{
if (File.Exists(DefaultConfigFile))
{
return FromFile(DefaultConfigFile);
}
Console.WriteLine($"Semantic kernel settings '{DefaultConfigFile}' not found, attempting to load configuration from user secrets.");
return FromUserSecrets();
}
catch (InvalidDataException ide)
{
Console.Error.WriteLine(
"Unable to load semantic kernel settings, please provide configuration settings using instructions in the README.\n" +
"Please refer to: https://github.com/microsoft/semantic-kernel-starters/blob/main/sk-csharp-hello-world/README.md#configuring-the-starter"
);
throw new InvalidOperationException(ide.Message);
}
}
/// <summary>
/// Load the kernel settings from the specified configuration file if it exists.
/// </summary>
internal static KernelSettings FromFile(string configFile = DefaultConfigFile)
{
if (!File.Exists(configFile))
{
throw new FileNotFoundException($"Configuration not found: {configFile}");
}
var configuration = new ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile(configFile, optional: true, reloadOnChange: true)
.Build();
return configuration.Get<KernelSettings>()
?? throw new InvalidDataException($"Invalid semantic kernel settings in '{configFile}', please provide configuration settings using instructions in the README.");
}
/// <summary>
/// Load the kernel settings from user secrets.
/// </summary>
internal static KernelSettings FromUserSecrets()
{
var configuration = new ConfigurationBuilder()
.AddUserSecrets<KernelSettings>()
.Build();
return configuration.Get<KernelSettings>()
?? throw new InvalidDataException("Invalid semantic kernel settings in user secrets, please provide configuration settings using instructions in the README.");
}
}

View File

@@ -59,7 +59,7 @@
"type": "func",
"dependsOn": "build (functions)",
"options": {
"cwd": "${workspaceFolder}/bin/Debug/net6.0"
"cwd": "${workspaceFolder}/bin/Debug/net7.0"
},
"command": "host start",
"isBackground": true,

View File

@@ -6,6 +6,7 @@ using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using Models;
using skills;
public class ExecuteFunctionEndpoint
{
@@ -31,27 +32,14 @@ public class ExecuteFunctionEndpoint
HttpRequestData requestData,
FunctionContext executionContext, string skillName, string functionName)
{
#pragma warning disable CA1062
try
{
var functionRequest = await JsonSerializer.DeserializeAsync<ExecuteFunctionRequest>(requestData.Body, s_jsonOptions).ConfigureAwait(false);
#pragma warning disable CA1062
// note: using skills from the repo
var skillsDirectory = Path.Combine(System.IO.Directory.GetCurrentDirectory(), "skills");
var skillDirectory = Path.Combine(skillsDirectory, skillName);
if (!System.IO.Directory.Exists(skillDirectory))
{
return await CreateResponseAsync(requestData, HttpStatusCode.NotFound, new ErrorResponse() { Message = $"Unable to find {skillName}" }).ConfigureAwait(false);
}
var skill = this._kernel.ImportSemanticSkillFromDirectory(skillsDirectory, skillName);
if (!skill.ContainsKey(functionName))
{
return await CreateResponseAsync(requestData, HttpStatusCode.NotFound, new ErrorResponse() { Message = $"Unable to find {skillName}.{functionName}" }).ConfigureAwait(false);
}
var function = skill[functionName];
var skillConfig = SemanticFunctionConfig.ForSkillAndFunction(skillName, functionName);
var function = _kernel.CreateSemanticFunction(skillConfig.PromptTemplate, skillConfig.Name, skillConfig.SkillName,
skillConfig.Description, skillConfig.MaxTokens, skillConfig.Temperature,
skillConfig.TopP, skillConfig.PPenalty, skillConfig.FPenalty);
var context = new ContextVariables();
foreach (var v in functionRequest.Variables)
@@ -71,8 +59,6 @@ public class ExecuteFunctionEndpoint
return await CreateResponseAsync(requestData, HttpStatusCode.BadRequest, new ErrorResponse() { Message = $"Invalid request body." }).ConfigureAwait(false);
}
}
private static async Task<HttpResponseData> CreateResponseAsync(HttpRequestData requestData, HttpStatusCode statusCode, object responseBody)

View File

@@ -6,11 +6,11 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace></RootNamespace>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<LangVersion>10</LangVersion>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
@@ -24,8 +24,10 @@
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.OpenApi" Version="2.0.0-preview2" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.OpenApi" Version="2.0.0-preview2" TreatAsUsed="true" />
<PackageReference Include="Microsoft.SemanticKernel" Version="0.14.547.1-preview" />
<ProjectReference Include="..\skills\skills.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -37,9 +39,6 @@
<None Update="config\appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="skills\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -1,12 +0,0 @@
{
"schema": 1,
"description": "From a simple description of an application output a development plan for building the application.",
"type": "completion",
"completion": {
"max_tokens": 4096,
"temperature": 0.0,
"top_p": 0.0,
"presence_penalty": 0.0,
"frequency_penalty": 0.0
}
}

View File

@@ -1,9 +0,0 @@
You are a Dev Lead for an application team, building the application described below.
Please break down the steps and modules required to develop the complete application, describe each step in detail.
Make prescriptive architecture, language, and frameowrk choices, do not provide a range of choices.
For each step or module then break down the steps or subtasks required to complete that step or module.
For each subtask write an LLM prompt that would be used to tell a model to write the coee that will accomplish that subtask. If the subtask involves taking action/running commands tell the model to write the script that will run those commands.
In each LLM prompt restrict the model from outputting other text that is not in the form of code or code comments.
Please output a JSON data structure with a list of steps and a description of each step, and the steps or subtasks that each requires, and the LLM prompts for each subtask.
Do not output any other text.
Input: {{$input}}

View File

@@ -1,12 +0,0 @@
{
"schema": 1,
"description": "From a description of a coding task out put the code or scripts necessary to complete the task.",
"type": "completion",
"completion": {
"max_tokens": 4096,
"temperature": 0.0,
"top_p": 0.0,
"presence_penalty": 0.0,
"frequency_penalty": 0.0
}
}

View File

@@ -1,6 +0,0 @@
You are a Developer for an application.
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}}

View File

@@ -1,12 +0,0 @@
{
"schema": 1,
"description": "Output a script that will help bootstrap a new code project in .NET",
"type": "completion",
"completion": {
"max_tokens": 7000,
"temperature": 0.0,
"top_p": 0.0,
"presence_penalty": 0.0,
"frequency_penalty": 0.0
}
}

View File

@@ -1,5 +0,0 @@
Please write a bash script with the commands that would be required to generate applications as described in the following input.
You may add comments to the script and the generated output but do not add any other text except the bash script.
You may include commands to build the applications but do not run them.
Do not include any git commands.
Input: {{$input}}

View File

@@ -1,12 +0,0 @@
{
"schema": 1,
"description": "From a simple description output a README.md file for a GitHub repository.",
"type": "completion",
"completion": {
"max_tokens": 7600,
"temperature": 0.0,
"top_p": 0.0,
"presence_penalty": 0.0,
"frequency_penalty": 0.0
}
}

View File

@@ -1,5 +0,0 @@
You are a program manager on a software development team. You are working on an app described below.
Based on the input below, and any dialog or other context, please output a raw README.MD markdown file documenting the main features of the app and the architecture or code organization.
Do not describe how to create the application.
Write the README as if it were documenting the features and architecture of the application. You may include instructions for how to run the application.
Input: {{$input}}

25
skills/DevLead.cs Normal file
View File

@@ -0,0 +1,25 @@
namespace skills;
public static class DevLead {
public static SemanticFunctionConfig Plan = new SemanticFunctionConfig
{
PromptTemplate = """
You are a Dev Lead for an application team, building the application described below.
Please break down the steps and modules required to develop the complete application, describe each step in detail.
Make prescriptive architecture, language, and frameowrk choices, do not provide a range of choices.
For each step or module then break down the steps or subtasks required to complete that step or module.
For each subtask write an LLM prompt that would be used to tell a model to write the coee that will accomplish that subtask. If the subtask involves taking action/running commands tell the model to write the script that will run those commands.
In each LLM prompt restrict the model from outputting other text that is not in the form of code or code comments.
Please output a JSON data structure with a list of steps and a description of each step, and the steps or subtasks that each requires, and the LLM prompts for each subtask.
Do not output any other text.
Input: {{$input}}
""",
Name = nameof(Plan),
SkillName = nameof(DevLead),
Description = "From a simple description of an application output a development plan for building the application.",
MaxTokens = 4096,
Temperature = 0.0,
TopP = 0.0,
PPenalty = 0.0,
FPenalty = 0.0
};
}

23
skills/Developer.cs Normal file
View File

@@ -0,0 +1,23 @@
namespace skills;
public static class Developer {
public static SemanticFunctionConfig Implement = new SemanticFunctionConfig
{
PromptTemplate = """
You are a Developer for an application.
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}}
""",
Name = nameof(Implement),
SkillName = nameof(Developer),
Description = "From a description of a coding task out put the code or scripts necessary to complete the task.",
MaxTokens = 4096,
Temperature = 0.0,
TopP = 0.0,
PPenalty = 0.0,
FPenalty = 0.0
};
}

40
skills/PM.cs Normal file
View File

@@ -0,0 +1,40 @@
namespace skills;
public static class PM
{
public static SemanticFunctionConfig BootstrapProject = new SemanticFunctionConfig
{
PromptTemplate = """
Please write a bash script with the commands that would be required to generate applications as described in the following input.
You may add comments to the script and the generated output but do not add any other text except the bash script.
You may include commands to build the applications but do not run them.
Do not include any git commands.
Input: {{$input}}
""",
Name = nameof(BootstrapProject),
SkillName = nameof(PM),
Description = "Bootstrap a new project",
MaxTokens = 7000,
Temperature = 0.0,
TopP = 0.0,
PPenalty = 0.0,
FPenalty = 0.0
};
public static SemanticFunctionConfig Readme = new SemanticFunctionConfig
{
PromptTemplate = """
You are a program manager on a software development team. You are working on an app described below.
Based on the input below, and any dialog or other context, please output a raw README.MD markdown file documenting the main features of the app and the architecture or code organization.
Do not describe how to create the application.
Write the README as if it were documenting the features and architecture of the application. You may include instructions for how to run the application.
Input: {{$input}}
""",
Name = nameof(Readme),
SkillName = nameof(PM),
Description = "From a simple description output a README.md file for a GitHub repository.",
MaxTokens = 7600,
Temperature = 0.0,
TopP = 0.0,
PPenalty = 0.0,
FPenalty = 0.0
};
}

View File

@@ -0,0 +1,23 @@
namespace skills;
public class SemanticFunctionConfig
{
public string PromptTemplate { get; set; }
public string Name { get; set; }
public string SkillName { get; set; }
public string Description { get; set; }
public int MaxTokens { get; set; }
public double Temperature { get; set; }
public double TopP { get; set; }
public double PPenalty { get; set; }
public double FPenalty { get; set; }
public static SemanticFunctionConfig ForSkillAndFunction(string skillName, string functionName) =>
(skillName, functionName) switch
{
(nameof(PM), nameof(PM.BootstrapProject)) => PM.BootstrapProject,
(nameof(PM), nameof(PM.Readme)) => PM.Readme,
(nameof(DevLead), nameof(DevLead.Plan)) => DevLead.Plan,
(nameof(Developer), nameof(Developer.Implement)) => Developer.Implement,
_ => throw new ArgumentException($"Unable to find {skillName}.{functionName}")
};
}

13
skills/skills.csproj Normal file
View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>skills</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SemanticKernel" Version="0.14.547.1-preview" />
</ItemGroup>
</Project>