diff --git a/apps/SKonsole/Commands/ConfigCommand.cs b/apps/SKonsole/Commands/ConfigCommand.cs new file mode 100644 index 0000000..8a54581 --- /dev/null +++ b/apps/SKonsole/Commands/ConfigCommand.cs @@ -0,0 +1,102 @@ +using Newtonsoft.Json.Linq; +using Spectre.Console; +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SKonsole.Commands +{ + public class ConfigCommand : Command + { + private readonly ConfigurationProvider _config; + + public ConfigCommand(ConfigurationProvider config) : base("config", "skonsole configuration") + { + this._config = config; + Add(ConfigGetCommand()); + Add(ConfigSetCommand()); + + this.SetHandler(async context => await RunConfigAsync(context.GetCancellationToken())); + } + + Command ConfigGetCommand() + { + var keyArgument = new Argument("key", "configuration key"); + + var getCommand = new Command("get", "get configuration value"); + + getCommand.AddArgument(keyArgument); + + getCommand.SetHandler((key) => + { + var value = _config.Get(key); + if (value == null) + { + AnsiConsole.MarkupLine($"[red]Configuration key '{key}' not found.[/]"); + } + else + { + AnsiConsole.MarkupLine($"[green]{key}[/]: {value}"); + } + }, keyArgument); + return getCommand; + } + + + Command ConfigSetCommand() + { + var keyArgument = new Argument("key", "configuration key"); + var valueArgument = new Argument("value", "configuration value"); + + var setCommand = new Command("set", "set configuration value"); + setCommand.AddArgument(keyArgument); + setCommand.AddArgument(valueArgument); + + setCommand.SetHandler(_config.SaveConfig, keyArgument, valueArgument); + return setCommand; + } + + static async Task RunConfigAsync(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + var keys = new[] + { + "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", + "AZURE_OPENAI_API_ENDPOINT", + "AZURE_OPENAI_API_KEY" + }; + + var config = new ConfigurationProvider(); + var configKey = await new SelectionPrompt() + .Title("Select key to config:") + .AddChoices(keys) + .ShowAsync(AnsiConsole.Console, token); + + var currentValue = config.Get(configKey); + + var value = await new TextPrompt($"Set value for [green]{configKey}[/]") + .DefaultValue(currentValue ?? string.Empty) + .HideDefaultValue() + .Validate((value) => + { + if (string.IsNullOrWhiteSpace(value)) + { + return ValidationResult.Error("[red]Value cannot be empty[/]"); + } + return ValidationResult.Success(); + }) + .AllowEmpty() + .ShowAsync(AnsiConsole.Console, token); + if (!string.IsNullOrWhiteSpace(value)) + { + await config.SaveConfig(configKey, value.Trim()); + } + } + } + } + +} diff --git a/apps/SKonsole/ConfigurationProvider.cs b/apps/SKonsole/ConfigurationProvider.cs new file mode 100644 index 0000000..eab8148 --- /dev/null +++ b/apps/SKonsole/ConfigurationProvider.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.Configuration; +using System.Text.Json; + +public class ConfigurationProvider +{ + public static ConfigurationProvider Instance = new(); + + const string _file = ".skonsole"; + + private readonly string _path; + private readonly IConfiguration _configuration; + private readonly Dictionary _config = new(); + + public ConfigurationProvider() + { + var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + _path = Path.Combine(userProfile, _file); + + if (File.Exists(_path)) + { + _config = FromJson>(File.ReadAllText(_path)) ?? new(); + } + + _configuration = new ConfigurationBuilder() + .AddEnvironmentVariables() + .AddInMemoryCollection(_config) + .Build(); + } + + public async Task SaveConfig(string key, string value) + { + _config[key] = value; + + await File.WriteAllTextAsync(_path, ToJson(_config)); + } + + public string? Get(string key) + { + return _configuration[key]; + } + + private static string ToJson(T obj) + { + return JsonSerializer.Serialize(obj); + } + + private static T? FromJson(string json) + { + if (json == null) + return default; + try + { + return JsonSerializer.Deserialize(json); + } + catch + { + return default; + } + } +} \ No newline at end of file diff --git a/apps/SKonsole/Program.cs b/apps/SKonsole/Program.cs index 73ff38f..5d00b0d 100644 --- a/apps/SKonsole/Program.cs +++ b/apps/SKonsole/Program.cs @@ -1,4 +1,5 @@ using System.CommandLine; +using System.CommandLine.Builder; using System.Diagnostics; using System.Text; using Microsoft.Extensions.Logging; @@ -10,6 +11,7 @@ using Microsoft.SemanticKernel.SkillDefinition; using Microsoft.SemanticKernel.Skills.Web; using Microsoft.SemanticKernel.Skills.Web.Bing; +using SKonsole.Commands; using SKonsole.Skills; Console.OutputEncoding = Encoding.Unicode; @@ -37,20 +39,16 @@ UseExponentialBackoff = true, }); }) -// .WithAzureTextCompletionService( -// EnvVar("AZURE_OPENAI_DEPLOYMENT_NAME"), -// EnvVar("AZURE_OPENAI_API_ENDPOINT"), -// EnvVar("AZURE_OPENAI_API_KEY"), -// EnvVar("AZURE_OPENAI_DEPLOYMENT_LABEL")) .WithAzureChatCompletionService( - EnvVar("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"), - EnvVar("AZURE_OPENAI_API_ENDPOINT"), - EnvVar("AZURE_OPENAI_API_KEY")) + ConfigVar("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"), + ConfigVar("AZURE_OPENAI_API_ENDPOINT"), + ConfigVar("AZURE_OPENAI_API_KEY")) .WithLogger(kernelLogger) .Build(); _kernel.Logger.LogTrace("KernelSingleton.Instance: adding Azure OpenAI backends"); + var rootCommand = new RootCommand(); var commitCommand = new Command("commit", "Commit subcommand"); var prCommand = new Command("pr", "Pull Request feedback subcommand"); @@ -58,6 +56,8 @@ var prDescriptionCommand = new Command("description", "Pull Request description subcommand"); var plannerCommand = new Command("createPlan", "Planner subcommand"); var promptChatCommand = new Command("promptChat", "Prompt chat subcommand"); + + var messageArgument = new Argument ("message", "An argument that is parsed as a string."); @@ -79,6 +79,7 @@ prCommand.Add(prFeedbackCommand); prCommand.Add(prDescriptionCommand); +rootCommand.Add(new ConfigCommand(ConfigurationProvider.Instance)); rootCommand.Add(commitCommand); rootCommand.Add(prCommand); rootCommand.Add(plannerCommand); @@ -211,7 +212,7 @@ static async Task RunCreatePlan(IKernel kernel, ILogger? logger, string message) kernel.ImportSkill(new WriterSkill(kernel), "writer"); - var bingConnector = new BingConnector(EnvVar("BING_API_KEY")); + var bingConnector = new BingConnector(ConfigVar("BING_API_KEY")); var bing = new WebSearchEngineSkill(bingConnector); var search = kernel.ImportSkill(bing, "bing"); @@ -247,10 +248,11 @@ static async Task RunPromptChat(IKernel kernel, ILogger? logger) await RunChat(kernel, logger, chatFunction); } -static string EnvVar(string name) +static string ConfigVar(string name) { - var value = Environment.GetEnvironmentVariable(name); - if (string.IsNullOrEmpty(value)) throw new Exception($"Env var not set: {name}"); + var provider = ConfigurationProvider.Instance; + var value = provider.Get(name); + if (string.IsNullOrEmpty(value)) throw new Exception($"Configuration var not set: {name}.Please run `skonsole config` to set it."); return value; } @@ -270,7 +272,8 @@ static async Task RunChat(IKernel kernel, ILogger? logger, ISKFunction chatFunct (logger ?? kernel.Logger).LogInformation("{botMessage}", botMessageFormatted); (logger ?? kernel.Logger).LogInformation(">>>"); - userMessage = Console.ReadLine(); // TODO -- How to support multi-line input? + + userMessage = ReadMutiLineInput(); if (userMessage == "exit") break; history += $"{botMessageFormatted}Human: {userMessage}\nAI:"; @@ -278,4 +281,17 @@ static async Task RunChat(IKernel kernel, ILogger? logger, ISKFunction chatFunct botMessage = await kernel.RunAsync(contextVariables, chatFunction); } +} + +static string ReadMutiLineInput() +{ + var input = new StringBuilder(); + var line = string.Empty; + + while ((line = Console.ReadLine()) != string.Empty) + { + input.AppendLine(line); + } + + return input.ToString().Trim(); } \ No newline at end of file diff --git a/apps/SKonsole/SKonsole.csproj b/apps/SKonsole/SKonsole.csproj index 3876bc9..38f295f 100644 --- a/apps/SKonsole/SKonsole.csproj +++ b/apps/SKonsole/SKonsole.csproj @@ -8,9 +8,11 @@ skonsole + +