cli calls local skills, instead of http function call

This commit is contained in:
Kosta Petan
2023-06-09 21:38:21 +02:00
parent f8485dc011
commit 45983b9366
6 changed files with 164 additions and 45 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,6 +9,9 @@
</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" />

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

@@ -11,7 +11,7 @@ public class SemanticFunctionConfig
public double TopP { get; set; }
public double PPenalty { get; set; }
public double FPenalty { get; set; }
private static SemanticFunctionConfig ForSkillAndFunction(string skillName, string functionName) =>
public static SemanticFunctionConfig ForSkillAndFunction(string skillName, string functionName) =>
(skillName, functionName) switch
{
(nameof(PM), nameof(PM.BootstrapProject)) => PM.BootstrapProject,