diff --git a/.vscode/launch.json b/.vscode/launch.json index 4d302d2..dfd58b6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,11 +10,9 @@ "request": "launch", "preLaunchTask": "build", "program": "${workspaceFolder}/apps/SKonsole/bin/Debug/net7.0/SKonsole.dll", - "args": [ - /*"commit"*/ - ], + "args": ["stepwise", "optionset", "bing++"], "cwd": "${workspaceFolder}", - "console": "internalConsole", + "console": "integratedTerminal", "stopAtEntry": false }, { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 21b4385..d9853b3 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,41 +1,52 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "build", - "command": "dotnet", - "type": "process", - "args": [ - "build", - "${workspaceFolder}/apps/SKonsole/SKonsole.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/apps/SKonsole/SKonsole.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "--project", - "${workspaceFolder}/apps/SKonsole/SKonsole.csproj" - ], - "problemMatcher": "$msCompile" - } - ] -} \ No newline at end of file + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/apps/SKonsole/SKonsole.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "format", + "command": "dotnet", + "type": "process", + "args": ["format", "${workspaceFolder}/skonsole.sln"], + "problemMatcher": "$msCompile", + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/apps/SKonsole/SKonsole.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/apps/SKonsole/SKonsole.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/Directory.Packages.props b/Directory.Packages.props index 288cf5d..90e2215 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,10 +8,10 @@ - - - - + + + + diff --git a/apps/SKonsole/Commands/CommitCommand.cs b/apps/SKonsole/Commands/CommitCommand.cs index 0e10576..deff8be 100644 --- a/apps/SKonsole/Commands/CommitCommand.cs +++ b/apps/SKonsole/Commands/CommitCommand.cs @@ -98,9 +98,9 @@ private static async Task RunCommitMessage(CancellationToken token, ILogger logg } } - var pullRequestSkill = kernel.ImportSkill(new PRSkill.PullRequestSkill(kernel)); + var pullRequestSkill = kernel.ImportFunctions(new PRSkill.PullRequestSkill(kernel)); - void HorizontalRule(string title, string style = "white bold") + static void HorizontalRule(string title, string style = "white bold") { AnsiConsole.WriteLine(); AnsiConsole.Write(new Rule($"[{style}]{title}[/]").RuleStyle("grey").LeftJustified()); @@ -121,7 +121,7 @@ void HorizontalRule(string title, string style = "white bold") var kernelResponse = await kernel.RunAsync(output, token, pullRequestSkill["GenerateCommitMessage"]); task.StopTask(); - var result = kernelResponse.Result; + var result = kernelResponse.GetValue() ?? string.Empty; await ClipboardService.SetTextAsync(result); return result; }); diff --git a/apps/SKonsole/Commands/PRCommand.cs b/apps/SKonsole/Commands/PRCommand.cs index 6cb0364..a1e5439 100644 --- a/apps/SKonsole/Commands/PRCommand.cs +++ b/apps/SKonsole/Commands/PRCommand.cs @@ -61,6 +61,7 @@ public PRCommand(ConfigurationProvider config, ILogger? logger = null) : base("p { return context.ParseResult.GetValueForOption(option); } + private Command GeneratePRFeedbackCommand(Option targetBranchOption) { var prFeedbackCommand = new Command("feedback", "Pull Request feedback subcommand"); @@ -106,11 +107,11 @@ private static async Task RunPullRequestFeedback(CancellationToken token, ILogge string output = await process.StandardOutput.ReadToEndAsync(); - var pullRequestSkill = kernel.ImportSkill(new PRSkill.PullRequestSkill(kernel)); + var pullRequestSkill = kernel.ImportFunctions(new PRSkill.PullRequestSkill(kernel)); var kernelResponse = await kernel.RunAsync(output, token, pullRequestSkill["GeneratePullRequestFeedback"]); - logger.LogInformation("Pull Request Feedback:\n{result}", kernelResponse.Result); + logger.LogInformation("Pull Request Feedback:\n{result}", kernelResponse.GetValue()); } private static async Task RunPullRequestDescription(CancellationToken token, ILogger logger, string targetBranch = "origin/main", string outputFormat = "", string outputFile = "", string diffInputFile = "") @@ -119,13 +120,13 @@ private static async Task RunPullRequestDescription(CancellationToken token, ILo var output = await FetchDiff(targetBranch, diffInputFile); - var pullRequestSkill = kernel.ImportSkill(new PRSkill.PullRequestSkill(kernel)); + var pullRequestSkill = kernel.ImportFunctions(new PRSkill.PullRequestSkill(kernel)); var contextVariables = new ContextVariables(output); contextVariables.Set("outputFormatInstructions", PRSkill.Utils.FormatInstructionsProvider.GetOutputFormatInstructions(outputFormat)); var kernelResponse = await kernel.RunAsync(contextVariables, token, pullRequestSkill["GeneratePR"]); - logger.LogInformation("Pull Request Description:\n{result}", kernelResponse.Result); + logger.LogInformation("Pull Request Description:\n{result}", kernelResponse.GetValue()); if (!string.IsNullOrEmpty(outputFile)) { @@ -134,7 +135,7 @@ private static async Task RunPullRequestDescription(CancellationToken token, ILo { Directory.CreateDirectory(directory); } - System.IO.File.WriteAllText(outputFile, kernelResponse.Result); + System.IO.File.WriteAllText(outputFile, kernelResponse.GetValue()); } } diff --git a/apps/SKonsole/Commands/PlannerCommand.cs b/apps/SKonsole/Commands/PlannerCommand.cs index ffb2477..7191ade 100644 --- a/apps/SKonsole/Commands/PlannerCommand.cs +++ b/apps/SKonsole/Commands/PlannerCommand.cs @@ -1,9 +1,8 @@ using System.CommandLine; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Planning; -using Microsoft.SemanticKernel.Skills.Web; -using Microsoft.SemanticKernel.Skills.Web.Bing; +using Microsoft.SemanticKernel.Planners; +using Microsoft.SemanticKernel.Plugins.Web; +using Microsoft.SemanticKernel.Plugins.Web.Bing; using SKonsole.Skills; using SKonsole.Utils; @@ -31,8 +30,10 @@ private Command GenerateCreatePlanCommand() { var messageArgument = new Argument ("message", "An argument that is parsed as a string."); - var createPlanCommand = new Command("create", "Create Plan subcommand"); - createPlanCommand.Add(messageArgument); + var createPlanCommand = new Command("create", "Create Plan subcommand") + { + messageArgument + }; createPlanCommand.SetHandler(async (messageArgumentValue) => await RunCreatePlan(CancellationToken.None, this._logger, messageArgumentValue), messageArgument); return createPlanCommand; } @@ -42,22 +43,22 @@ private static async Task RunCreatePlan(CancellationToken token, ILogger logger, var kernel = KernelProvider.Instance.Get(); // Eventually, Kernel will be smarter about what skills it uses for an ask. - // kernel.ImportSkill(new EmailSkill(), "email"); - // kernel.ImportSkill(new GitSkill(), "git"); - // kernel.ImportSkill(new SearchUrlSkill(), "url"); - // kernel.ImportSkill(new HttpSkill(), "http"); - // kernel.ImportSkill(new PRSkill.PullRequestSkill(kernel), "PullRequest"); + // kernel.ImportFunctions(new EmailSkill(), "email"); + // kernel.ImportFunctions(new GitSkill(), "git"); + // kernel.ImportFunctions(new SearchUrlSkill(), "url"); + // kernel.ImportFunctions(new HttpSkill(), "http"); + // kernel.ImportFunctions(new PRSkill.PullRequestSkill(kernel), "PullRequest"); - kernel.ImportSkill(new WriterSkill(kernel), "writer"); + kernel.ImportFunctions(new WriterSkill(kernel), "writer"); var bingConnector = new BingConnector(Configuration.ConfigVar("BING_API_KEY")); - var bing = new WebSearchEngineSkill(bingConnector); - var search = kernel.ImportSkill(bing, "bing"); + var bing = new WebSearchEnginePlugin(bingConnector); + var search = kernel.ImportFunctions(bing, "bing"); // var planner = new ActionPlanner(); var planner = new SequentialPlanner(kernel); var plan = await planner.CreatePlanAsync(message); - await plan.InvokeAsync(); + await kernel.RunAsync(plan); } private readonly ILogger _logger; diff --git a/apps/SKonsole/Commands/PromptChatCommand.cs b/apps/SKonsole/Commands/PromptChatCommand.cs index d2a12c9..edc4328 100644 --- a/apps/SKonsole/Commands/PromptChatCommand.cs +++ b/apps/SKonsole/Commands/PromptChatCommand.cs @@ -2,9 +2,9 @@ using System.Text; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.SemanticFunctions; -using Microsoft.SemanticKernel.SkillDefinition; using SKonsole.Utils; using Spectre.Console; @@ -46,16 +46,19 @@ private static async Task RunPromptChat(CancellationToken token, ILogger logger) AI: "; - var promptConfig = new PromptTemplateConfig - { - Completion = + var promptConfig = new PromptTemplateConfig(); + promptConfig.ModelSettings.Add( + new AIRequestSettings() { - MaxTokens = 2000, - Temperature = 0.7, - TopP = 0.5, - StopSequences = new List { "Human:", "AI:" }, + ExtensionData = new Dictionary() + { + { "Temperature", 0.7 }, + { "TopP", 0.5 }, + { "MaxTokens", 2000 }, + { "StopSequences", new List { "Human:", "AI:" } } + } } - }; + ); var promptTemplate = new PromptTemplate(SkPrompt, promptConfig, kernel); var functionConfig = new SemanticFunctionConfig(promptConfig, promptTemplate); var chatFunction = kernel.RegisterSemanticFunction("PromptBot", "Chat", functionConfig); @@ -74,7 +77,7 @@ private static async Task RunChat(IKernel kernel, ILogger? logger, ISKFunction c var botMessage = await kernel.RunAsync(contextVariables, chatFunction); var userMessage = string.Empty; - void HorizontalRule(string title, string style = "white bold") + static void HorizontalRule(string title, string style = "white bold") { AnsiConsole.WriteLine(); AnsiConsole.Write(new Rule($"[{style}]{title}[/]").RuleStyle("grey").LeftJustified()); @@ -85,7 +88,7 @@ void HorizontalRule(string title, string style = "white bold") { HorizontalRule("AI", "green bold"); AnsiConsole.Foreground = ConsoleColor.Green; - AnsiConsole.WriteLine(botMessage.ToString()); + AnsiConsole.WriteLine(botMessage?.ToString() ?? "NO MESSAGE FROM BOT"); AnsiConsole.ResetColors(); HorizontalRule("User"); diff --git a/apps/SKonsole/Commands/StepwisePlannerCommand.cs b/apps/SKonsole/Commands/StepwisePlannerCommand.cs index 97faee5..28429ae 100644 --- a/apps/SKonsole/Commands/StepwisePlannerCommand.cs +++ b/apps/SKonsole/Commands/StepwisePlannerCommand.cs @@ -4,11 +4,11 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Orchestration; -using Microsoft.SemanticKernel.Planning; -using Microsoft.SemanticKernel.SkillDefinition; -using Microsoft.SemanticKernel.Skills.Core; -using Microsoft.SemanticKernel.Skills.Web; -using Microsoft.SemanticKernel.Skills.Web.Bing; +using Microsoft.SemanticKernel.Planners; +using Microsoft.SemanticKernel.Plugins.Core; +using Microsoft.SemanticKernel.Plugins.Web; +using Microsoft.SemanticKernel.Plugins.Web.Bing; +using SKonsole.Skills; using SKonsole.Utils; using Spectre.Console; @@ -38,7 +38,7 @@ private static async Task RunCreatePlan(CancellationToken token, ILogger logger, IKernel kernel = LoadOptionSet(optionSet); var stepKernel = KernelProvider.Instance.Get(); - var functions = stepKernel.ImportSkill(new StepwiseSkill(kernel), "stepwise"); + var functions = stepKernel.ImportFunctions(new StepwiseSkill(kernel), "stepwise"); await RunChat(stepKernel, null, functions["RespondTo"]).ConfigureAwait(false); } @@ -50,31 +50,31 @@ private static IKernel LoadOptionSet(string optionSet) if (optionSet.Contains("bing")) { var bingConnector = new BingConnector(Configuration.ConfigVar("BING_API_KEY")); - var bing = new WebSearchEngineSkill(bingConnector); - var search = kernel.ImportSkill(bing, "bing"); + var bing = new WebSearchEnginePlugin(bingConnector); + var search = kernel.ImportFunctions(bing, "bing"); } if (optionSet.Contains("++")) { - kernel.ImportSkill(new TimeSkill(), "time"); - kernel.ImportSkill(new ConversationSummarySkill(kernel), "summary"); - kernel.ImportSkill(new FileIOSkill(), "file"); + kernel.ImportFunctions(new TimePlugin(), "time"); + kernel.ImportFunctions(new ConversationSummaryPlugin(kernel), "summary"); + kernel.ImportFunctions(new SuperFileIOPlugin(), "file"); } else { if (optionSet.Contains("time")) { - kernel.ImportSkill(new TimeSkill(), "time"); + kernel.ImportFunctions(new TimePlugin(), "time"); } if (optionSet.Contains("summary")) { - kernel.ImportSkill(new ConversationSummarySkill(kernel), "summary"); + kernel.ImportFunctions(new ConversationSummaryPlugin(kernel), "summary"); } if (optionSet.Contains("file")) { - kernel.ImportSkill(new FileIOSkill(), "file"); + kernel.ImportFunctions(new SuperFileIOPlugin(), "file"); } } @@ -90,7 +90,7 @@ public StepwiseSkill(IKernel kernel) } [SKFunction, Description("Respond to a message.")] - public async Task RespondTo(string message, string history) + public async Task RespondTo(string message, string history) { var planner = new StepwisePlanner(this._kernel); @@ -103,10 +103,24 @@ public async Task RespondTo(string message, string history) // var result = await plan.InvokeAsync(); // Option 3 - Respond to the history with prompt - var plan2 = planner.CreatePlan($"{history}\n---\nGiven the conversation history, respond to the most recent message."); - var result = await plan2.InvokeAsync(); + var plan = planner.CreatePlan($"{history}\n---\nGiven the conversation history, respond to the most recent message."); + var result = await this._kernel.RunAsync(plan); - return result; + // Extract metadata and result string into new SKContext -- Is there a better way? + var functionResult = result?.FunctionResults?.FirstOrDefault(); + if (functionResult == null) + { + return null; + } + + var context = this._kernel.CreateNewContext(); + context.Variables.Update(functionResult.GetValue()); + foreach (var key in functionResult.Metadata.Keys) + { + context.Variables.Set(key, functionResult.Metadata[key]?.ToString()); + } + + return context; } } @@ -119,13 +133,11 @@ private static async Task RunChat(IKernel kernel, ILogger? logger, ISKFunction c var history = string.Empty; contextVariables.Set("history", history); - var botMessage = kernel.CreateNewContext(); - botMessage.Variables.Update("Hello!"); - //var botMessage = await kernel.RunAsync(contextVariables, chatFunction); + KernelResult botMessage = KernelResult.FromFunctionResults("Hello!", new List()); var userMessage = string.Empty; - void HorizontalRule(string title, string style = "white bold") + static void HorizontalRule(string title, string style = "white bold") { AnsiConsole.WriteLine(); AnsiConsole.Write(new Rule($"[{style}]{title}[/]").RuleStyle("grey").LeftJustified()); @@ -134,9 +146,10 @@ void HorizontalRule(string title, string style = "white bold") while (userMessage != "exit") { - if (botMessage.Variables.TryGetValue("skillCount", out string? skillCount) && skillCount != "0 ()") + var functionResult = botMessage.FunctionResults.FirstOrDefault(); + if (functionResult is not null && functionResult.TryGetMetadataValue("functionCount", out string? functionCount) && functionCount != "0 ()") { - HorizontalRule($"AI - {skillCount}", "green bold"); + HorizontalRule($"AI - {functionCount}", "green bold"); } else { @@ -144,7 +157,15 @@ void HorizontalRule(string title, string style = "white bold") } AnsiConsole.Foreground = ConsoleColor.Green; - AnsiConsole.WriteLine(botMessage.ToString()); + var message = botMessage.GetValue() ?? string.Empty; + if (message.Contains("Result not found")) + { + if (functionResult is not null && functionResult.TryGetMetadataValue("stepsTaken", out string? stepsTaken)) + { + message += $"\n{stepsTaken}"; + } + } + AnsiConsole.WriteLine(message); AnsiConsole.ResetColors(); HorizontalRule("User"); @@ -155,11 +176,11 @@ void HorizontalRule(string title, string style = "white bold") break; } - history += $"AI: {botMessage}\nHuman: {userMessage} \n"; + history += $"AI: {botMessage.GetValue()}\nHuman: {userMessage} \n"; contextVariables.Set("history", history); contextVariables.Set("message", userMessage); - botMessage = await AnsiConsole.Progress() + var kernelResult = await AnsiConsole.Progress() .AutoClear(true) .Columns(new ProgressColumn[] { @@ -175,6 +196,7 @@ void HorizontalRule(string title, string style = "white bold") task.StopTask(); return result; }); + botMessage = kernelResult ?? botMessage; } } diff --git a/apps/SKonsole/KernelProvider.cs b/apps/SKonsole/KernelProvider.cs index 77f3d27..24e2679 100644 --- a/apps/SKonsole/KernelProvider.cs +++ b/apps/SKonsole/KernelProvider.cs @@ -14,22 +14,16 @@ public IKernel Get() { var kernelBuilder = Kernel.Builder; - switch (Configuration.ConfigOption(ConfigConstants.LLM_PROVIDER)) + kernelBuilder = Configuration.ConfigOption(ConfigConstants.LLM_PROVIDER) switch { - case ConfigConstants.OpenAI: - kernelBuilder = kernelBuilder.WithOpenAIChatCompletionService( - Configuration.ConfigVar(ConfigConstants.OPENAI_CHAT_MODEL_ID), - Configuration.ConfigVar(ConfigConstants.OPENAI_API_KEY)); - break; - case ConfigConstants.AzureOpenAI: - default: - kernelBuilder = kernelBuilder.WithAzureChatCompletionService( - Configuration.ConfigVar(ConfigConstants.AZURE_OPENAI_CHAT_DEPLOYMENT_NAME), - Configuration.ConfigVar(ConfigConstants.AZURE_OPENAI_API_ENDPOINT), - Configuration.ConfigVar(ConfigConstants.AZURE_OPENAI_API_KEY)); - break; - } - + ConfigConstants.OpenAI => kernelBuilder.WithOpenAIChatCompletionService( + Configuration.ConfigVar(ConfigConstants.OPENAI_CHAT_MODEL_ID), + Configuration.ConfigVar(ConfigConstants.OPENAI_API_KEY)), + _ => kernelBuilder.WithAzureChatCompletionService( + Configuration.ConfigVar(ConfigConstants.AZURE_OPENAI_CHAT_DEPLOYMENT_NAME), + Configuration.ConfigVar(ConfigConstants.AZURE_OPENAI_API_ENDPOINT), + Configuration.ConfigVar(ConfigConstants.AZURE_OPENAI_API_KEY)), + }; var _kernel = kernelBuilder .WithRetryBasic(new() { diff --git a/apps/SKonsole/Program.cs b/apps/SKonsole/Program.cs index a434161..458b9fe 100644 --- a/apps/SKonsole/Program.cs +++ b/apps/SKonsole/Program.cs @@ -6,13 +6,14 @@ Console.OutputEncoding = Encoding.Unicode; -var rootCommand = new RootCommand(); - -rootCommand.Add(new ConfigCommand(ConfigurationProvider.Instance)); -rootCommand.Add(new CommitCommand(ConfigurationProvider.Instance)); -rootCommand.Add(new PRCommand(ConfigurationProvider.Instance)); -rootCommand.Add(new PlannerCommand(ConfigurationProvider.Instance)); -rootCommand.Add(new StepwisePlannerCommand(ConfigurationProvider.Instance)); -rootCommand.Add(new PromptChatCommand(ConfigurationProvider.Instance)); +var rootCommand = new RootCommand +{ + new ConfigCommand(ConfigurationProvider.Instance), + new CommitCommand(ConfigurationProvider.Instance), + new PRCommand(ConfigurationProvider.Instance), + new PlannerCommand(ConfigurationProvider.Instance), + new StepwisePlannerCommand(ConfigurationProvider.Instance), + new PromptChatCommand(ConfigurationProvider.Instance) +}; return await rootCommand.InvokeAsync(args); diff --git a/apps/SKonsole/SKonsole.csproj b/apps/SKonsole/SKonsole.csproj index a3d319c..c5a05bf 100644 --- a/apps/SKonsole/SKonsole.csproj +++ b/apps/SKonsole/SKonsole.csproj @@ -17,8 +17,8 @@ - - + + diff --git a/apps/SKonsole/Skills/EmailSkill.cs b/apps/SKonsole/Skills/EmailSkill.cs index 20127de..328f912 100644 --- a/apps/SKonsole/Skills/EmailSkill.cs +++ b/apps/SKonsole/Skills/EmailSkill.cs @@ -1,7 +1,7 @@ using System.ComponentModel; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Orchestration; -using Microsoft.SemanticKernel.SkillDefinition; namespace SKonsole.Skills; diff --git a/apps/SKonsole/Skills/GitSkill.cs b/apps/SKonsole/Skills/GitSkill.cs index b296dc7..7a23768 100644 --- a/apps/SKonsole/Skills/GitSkill.cs +++ b/apps/SKonsole/Skills/GitSkill.cs @@ -1,7 +1,7 @@ using System.ComponentModel; using System.Diagnostics; +using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Orchestration; -using Microsoft.SemanticKernel.SkillDefinition; namespace SKonsole.Skills; diff --git a/apps/SKonsole/Skills/SuperFileIOPlugin.cs b/apps/SKonsole/Skills/SuperFileIOPlugin.cs new file mode 100644 index 0000000..0481746 --- /dev/null +++ b/apps/SKonsole/Skills/SuperFileIOPlugin.cs @@ -0,0 +1,89 @@ + +using System.ComponentModel; +using System.Text; +using Microsoft.SemanticKernel; + +namespace SKonsole.Skills; + +public class SuperFileIOPlugin +{ + /// + /// Read a file + /// + /// + /// {{file.readAsync $path }} => "hello world" + /// + /// Source file + /// File content + [SKFunction, Description("Read a file")] + public async Task ReadAsync([Description("Source file")] string path) + { + path = string.IsNullOrEmpty(path) ? Directory.GetCurrentDirectory() : path; + using var reader = File.OpenText(path); + return await reader.ReadToEndAsync().ConfigureAwait(false); + } + + [SKFunction, Description("List files in a directory")] + public string List([Description("Source directory")] string path) + { + path = string.IsNullOrEmpty(path) ? Directory.GetCurrentDirectory() : path; + var files = Directory.GetFiles(path); + return string.Join("\n", files); + } + + [SKFunction, Description("List directories in a directory")] + public string ListDirs([Description("Source directory")] string path) + { + path = string.IsNullOrEmpty(path) ? Directory.GetCurrentDirectory() : path; + var files = Directory.GetFiles(path); + return string.Join("\n", files); + } + + [SKFunction, Description("Search files in a directory")] + public string Search([Description("Source directory")] string path, [Description("Search pattern")] string pattern) + { + path = string.IsNullOrEmpty(path) ? Directory.GetCurrentDirectory() : path; + var files = Directory.GetFiles(path, pattern); + return string.Join("\n", files); + } + + [SKFunction, Description("Search files in a directory, recursively")] + public string SearchAll([Description("Source directory")] string path, [Description("Search pattern")] string pattern) + { + path = string.IsNullOrEmpty(path) ? Directory.GetCurrentDirectory() : path; + var files = Directory.GetFiles(path, pattern, SearchOption.AllDirectories); + return string.Join("\n", files); + } + + [SKFunction, Description("Get current directory")] + public string CurrentDirectory() + { + return Directory.GetCurrentDirectory(); + } + + /// + /// Write a file + /// + /// + /// {{file.writeAsync}} + /// + /// The destination file path + /// The file content to write + /// An awaitable task + [SKFunction, Description("Write a file")] + public async Task WriteAsync( + [Description("Destination file")] string path, + [Description("File content")] string content) + { + path = string.IsNullOrEmpty(path) ? Directory.GetCurrentDirectory() : path; + byte[] text = Encoding.UTF8.GetBytes(content); + if (File.Exists(path) && File.GetAttributes(path).HasFlag(FileAttributes.ReadOnly)) + { + // Most environments will throw this with OpenWrite, but running inside docker on Linux will not. + throw new UnauthorizedAccessException($"File is read-only: {path}"); + } + + using var writer = File.OpenWrite(path); + await writer.WriteAsync(text, 0, text.Length).ConfigureAwait(false); + } +} diff --git a/apps/SKonsole/Skills/WriterSkill.cs b/apps/SKonsole/Skills/WriterSkill.cs index 79da1ba..8a38449 100644 --- a/apps/SKonsole/Skills/WriterSkill.cs +++ b/apps/SKonsole/Skills/WriterSkill.cs @@ -1,5 +1,5 @@ using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.SkillDefinition; +using Microsoft.SemanticKernel.AI; namespace SKonsole.Skills; @@ -13,11 +13,17 @@ public WriterSkill(IKernel kernel) { this._funnyPoemFunction = kernel.CreateSemanticFunction( FunnyPoemDefinition, - skillName: nameof(WriterSkill), + pluginName: nameof(WriterSkill), description: "Given a input topic or description or list, write a funny poem.", - maxTokens: MaxTokens, - temperature: 0.1, - topP: 0.5); + requestSettings: new AIRequestSettings() + { + ExtensionData = new Dictionary() + { + { "Temperature", 0.1 }, + { "TopP", 0.5 }, + { "MaxTokens", MaxTokens } + } + }); } private const string FunnyPoemDefinition = diff --git a/apps/SKonsole/Utils/Logging.cs b/apps/SKonsole/Utils/Logging.cs index 635d256..142cd35 100644 --- a/apps/SKonsole/Utils/Logging.cs +++ b/apps/SKonsole/Utils/Logging.cs @@ -10,11 +10,12 @@ internal static ILoggerFactory GetFactory() { builder .SetMinimumLevel(LogLevel.Information) + // .AddFilter("Microsoft.SemanticKernel.Planners.StepwisePlanner", LogLevel.Information) // Toggle to see chain of thought .AddFilter("Microsoft", LogLevel.Error) .AddFilter("AzureChatCompletion", LogLevel.Error) .AddFilter("System", LogLevel.Error) .AddFilter("SKonsole", LogLevel.Information) - .AddConsole(); + .AddSpectreConsole(); }); } } diff --git a/apps/SKonsole/Utils/SpectreConsoleExtensions.cs b/apps/SKonsole/Utils/SpectreConsoleExtensions.cs index 2ac72c2..8811a97 100644 --- a/apps/SKonsole/Utils/SpectreConsoleExtensions.cs +++ b/apps/SKonsole/Utils/SpectreConsoleExtensions.cs @@ -7,7 +7,7 @@ public static TextPrompt IsSecret(this TextPrompt obj, bool IsSecret, c { if (obj == null) { - throw new ArgumentNullException("obj"); + throw new ArgumentNullException(nameof(obj)); } obj.IsSecret = IsSecret; diff --git a/apps/SKonsole/Utils/SpectreConsoleLoggerProvider.cs b/apps/SKonsole/Utils/SpectreConsoleLoggerProvider.cs new file mode 100644 index 0000000..423af7f --- /dev/null +++ b/apps/SKonsole/Utils/SpectreConsoleLoggerProvider.cs @@ -0,0 +1,94 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Spectre.Console; + +namespace SKonsole.Utils; + +public sealed class SpectreConsoleLoggerProvider : ILoggerProvider +{ + public ILogger CreateLogger(string categoryName) + { + return new SpectreConsoleLogger(categoryName); + } + + public void Dispose() { } +} + +public sealed class SpectreConsoleLogger : ILogger, IDisposable +{ + private readonly string _categoryName; + + public SpectreConsoleLogger(string categoryName) + { + this._categoryName = categoryName; + } + + public IDisposable BeginScope(TState state) where TState : notnull => this; + + public void Dispose() + { + return; + } + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter) + { + if (!this.IsEnabled(logLevel)) + { + return; + } + + var message = formatter(state, exception); + + if (this._categoryName.EndsWith(".StepwisePlanner")) + { + var splitIndex = message.IndexOf(':'); + if (splitIndex == -1) + { + AnsiConsole.MarkupLineInterpolated($"[bold red]{message}[/]"); + return; + } + + var label = message.Substring(0, splitIndex); + var rest = message.Substring(splitIndex + 1); + + if (label == "Action") + { + if (rest.Contains("No action to take")) + { + return; + } + + AnsiConsole.MarkupLineInterpolated($"[bold blue]{label}:[/] {rest}"); + return; + } + + // if final answer, color is green + if (label == "Thought" && rest.StartsWith("Final answer:")) + { + AnsiConsole.MarkupLineInterpolated($"[bold green]{label}:[/] {rest}"); + return; + } + + AnsiConsole.MarkupLineInterpolated($"[bold yellow]{label}:[/] {rest}"); + return; + } + + AnsiConsole.MarkupLineInterpolated($"[bold]{logLevel}[/]: {this._categoryName}: {message}"); + } +} + +public static class SpectreConsoleLoggingExtensions +{ + public static ILoggingBuilder AddSpectreConsole(this ILoggingBuilder builder) + { + builder.Services.AddSingleton(); + return builder; + } +} diff --git a/skills/CondenseSkill/CondenseSkill.cs b/skills/CondenseSkill/CondenseSkill.cs index 1e1a417..0161296 100644 --- a/skills/CondenseSkill/CondenseSkill.cs +++ b/skills/CondenseSkill/CondenseSkill.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Orchestration; -using Microsoft.SemanticKernel.SkillDefinition; using Microsoft.SemanticKernel.Text; namespace CondenseSkillLib; @@ -23,7 +22,7 @@ public CondenseSkill(IKernel kernel) { // Load semantic skill defined with prompt templates var folder = CondenseSkillPath(); - var condenseSkill = kernel.ImportSemanticSkillFromDirectory(folder, SEMANTIC_FUNCTION_PATH); + var condenseSkill = kernel.ImportSemanticFunctionsFromDirectory(folder, SEMANTIC_FUNCTION_PATH); this._logger = kernel.LoggerFactory.CreateLogger(); } catch (Exception e) @@ -41,7 +40,7 @@ public async Task Condense( string separator = "", CancellationToken cancellationToken = default) { - var condenser = context.Skills.GetFunction(SEMANTIC_FUNCTION_PATH, "Condenser"); + var condenser = context.Functions.GetFunction(SEMANTIC_FUNCTION_PATH, "Condenser"); List lines = TextChunker.SplitPlainTextLines(input, CHUNK_SIZE / 8, EnglishRobertaTokenizer.Counter); List paragraphs = TextChunker.SplitPlainTextParagraphs(lines, CHUNK_SIZE, 100, tokenCounter: EnglishRobertaTokenizer.Counter); @@ -50,8 +49,8 @@ public async Task Condense( foreach (var paragraph in paragraphs) { context.Variables.Update(paragraph + separator); - context = await condenser.InvokeAsync(context, cancellationToken: cancellationToken); - condenseResult.Add(context.Result); + var result = await context.Runner.RunAsync(condenser, context.Variables, cancellationToken: cancellationToken); + condenseResult.Add(result.GetValue()); } if (paragraphs.Count <= 1) diff --git a/skills/PRSkill/PullRequestSkill.cs b/skills/PRSkill/PullRequestSkill.cs index ad7b845..d4d0625 100644 --- a/skills/PRSkill/PullRequestSkill.cs +++ b/skills/PRSkill/PullRequestSkill.cs @@ -5,9 +5,9 @@ using CondenseSkillLib; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.AI.TextCompletion; using Microsoft.SemanticKernel.Orchestration; -using Microsoft.SemanticKernel.SkillDefinition; using PRSkill.Utils; namespace PRSkill; @@ -20,9 +20,9 @@ public static async Task RollingChunkProcess(this ISKFunction func, L foreach (var chunk in chunkedInput) { context.Variables.Update(chunk); - context = await func.InvokeAsync(context); + var result = await context.Runner.RunAsync(func, context.Variables); - context.Variables.Set("previousresults", context.Result); + context.Variables.Set("previousresults", result.GetValue()); } return context; @@ -34,9 +34,9 @@ public static async Task CondenseChunkProcess(this ISKFunction func, foreach (var chunk in chunkedInput) { context.Variables.Update(chunk); - context = await func.InvokeAsync(context); + var result = await context.Runner.RunAsync(func, context.Variables); - results.Add(context.Result); + results.Add(result.GetValue()); } if (chunkedInput.Count <= 1) @@ -56,9 +56,9 @@ public static async Task AggregateChunkProcess(this ISKFunction func, foreach (var chunk in chunkedInput) { context.Variables.Update(chunk); - context = await func.InvokeAsync(context); + var result = await context.Runner.RunAsync(func, context.Variables); - results.Add(context.Result); + results.Add(result.GetValue()); } context.Variables.Update(string.Join("\n", results)); @@ -82,13 +82,13 @@ public PullRequestSkill(IKernel kernel) { // Load semantic skill defined with prompt templates var folder = PRSkillsPath(); - var PRSkill = kernel.ImportSemanticSkillFromDirectory(folder, SEMANTIC_FUNCTION_PATH); + var PRSkill = kernel.ImportSemanticFunctionsFromDirectory(folder, SEMANTIC_FUNCTION_PATH); this._condenseSkill = new CondenseSkill(kernel); this._kernel = Kernel.Builder .WithAIService(null, new RedirectTextCompletion(), true) .Build(); - this._kernel.ImportSemanticSkillFromDirectory(folder, SEMANTIC_FUNCTION_PATH); + this._kernel.ImportSemanticFunctionsFromDirectory(folder, SEMANTIC_FUNCTION_PATH); this._logger = this._kernel.LoggerFactory.CreateLogger(); } @@ -106,7 +106,7 @@ public async Task GeneratePullRequestFeedback( { this._logger.LogTrace("GeneratePullRequestFeedback called"); - var prFeedbackGenerator = context.Skills.GetFunction(SEMANTIC_FUNCTION_PATH, "PullRequestFeedbackGenerator"); + var prFeedbackGenerator = context.Functions.GetFunction(SEMANTIC_FUNCTION_PATH, "PullRequestFeedbackGenerator"); var chunkedInput = CommitChunker.ChunkCommitInfo(input, CHUNK_SIZE); return await prFeedbackGenerator.AggregateChunkProcess(chunkedInput, context); } @@ -120,10 +120,10 @@ public async Task GenerateCommitMessage( { this._logger.LogTrace("GenerateCommitMessage called"); - var commitGenerator = context.Skills.GetFunction(SEMANTIC_FUNCTION_PATH, "CommitMessageGenerator"); + var commitGenerator = context.Functions.GetFunction(SEMANTIC_FUNCTION_PATH, "CommitMessageGenerator"); - var commitGeneratorCapture = this._kernel.Skills.GetFunction(SEMANTIC_FUNCTION_PATH, "CommitMessageGenerator"); - var prompt = (await commitGeneratorCapture.InvokeAsync(cancellationToken: cancellationToken)).Result; + var commitGeneratorCapture = this._kernel.Functions.GetFunction(SEMANTIC_FUNCTION_PATH, "CommitMessageGenerator"); + var prompt = (await this._kernel.RunAsync(commitGeneratorCapture, cancellationToken: cancellationToken)).GetValue(); var chunkedInput = CommitChunker.ChunkCommitInfo(input, CHUNK_SIZE); return await commitGenerator.CondenseChunkProcess(this._condenseSkill, chunkedInput, prompt, context, "CommitMessageResult"); @@ -136,7 +136,7 @@ public async Task GeneratePR_Rolling( SKContext context, CancellationToken cancellationToken = default) { - var prGenerator_Rolling = context.Skills.GetFunction(SEMANTIC_FUNCTION_PATH, "PullRequestDescriptionGenerator_Rolling"); + var prGenerator_Rolling = context.Functions.GetFunction(SEMANTIC_FUNCTION_PATH, "PullRequestDescriptionGenerator_Rolling"); var chunkedInput = CommitChunker.ChunkCommitInfo(input, CHUNK_SIZE); return await prGenerator_Rolling.RollingChunkProcess(chunkedInput, context); } @@ -148,12 +148,12 @@ public async Task GeneratePR( SKContext context, CancellationToken cancellationToken = default) { - var prGenerator = context.Skills.GetFunction(SEMANTIC_FUNCTION_PATH, "PullRequestDescriptionGenerator"); + var prGenerator = context.Functions.GetFunction(SEMANTIC_FUNCTION_PATH, "PullRequestDescriptionGenerator"); - var prGeneratorCapture = this._kernel.Skills.GetFunction(SEMANTIC_FUNCTION_PATH, "PullRequestDescriptionGenerator"); + var prGeneratorCapture = this._kernel.Functions.GetFunction(SEMANTIC_FUNCTION_PATH, "PullRequestDescriptionGenerator"); var contextVariablesWithoutInput = context.Variables.Clone(); contextVariablesWithoutInput.Set("input", ""); - var prompt = (await prGeneratorCapture.InvokeAsync(variables: contextVariablesWithoutInput, cancellationToken: cancellationToken)).Result; + var prompt = (await this._kernel.RunAsync(prGeneratorCapture, contextVariablesWithoutInput, cancellationToken: cancellationToken)).GetValue(); var chunkedInput = CommitChunker.ChunkCommitInfo(input, CHUNK_SIZE); return await prGenerator.CondenseChunkProcess(this._condenseSkill, chunkedInput, prompt, context, "PullRequestDescriptionResult"); @@ -189,12 +189,12 @@ static bool SearchPath(string pathToFind, out string result, int maxAttempts = 1 public class RedirectTextCompletion : ITextCompletion { - Task> ITextCompletion.GetCompletionsAsync(string text, CompleteRequestSettings requestSettings, CancellationToken cancellationToken) + Task> ITextCompletion.GetCompletionsAsync(string text, AIRequestSettings requestSettings, CancellationToken cancellationToken) { return Task.FromResult>(new List { new RedirectTextCompletionResult(text) }); } - IAsyncEnumerable ITextCompletion.GetStreamingCompletionsAsync(string text, CompleteRequestSettings requestSettings, CancellationToken cancellationToken) + IAsyncEnumerable ITextCompletion.GetStreamingCompletionsAsync(string text, AIRequestSettings requestSettings, CancellationToken cancellationToken) { throw new NotImplementedException(); // TODO } diff --git a/skills/PRSkill/Utils/CommitParser.cs b/skills/PRSkill/Utils/CommitParser.cs index 341773a..62e4515 100644 --- a/skills/PRSkill/Utils/CommitParser.cs +++ b/skills/PRSkill/Utils/CommitParser.cs @@ -47,7 +47,7 @@ internal static class StringEx } var fileDiffMetadata = fileDiffMatch.Value; - var nextMatch = j == fileDiffMatches.Count - 1 || (fileDiffMatches[j + 1].Index) >= inputEnd ? inputEnd : fileDiffMatches[j + 1].Index; + var nextMatch = j == fileDiffMatches.Count - 1 || fileDiffMatches[j + 1].Index >= inputEnd ? inputEnd : fileDiffMatches[j + 1].Index; var fileDiff = input[(fileDiffMatch.Index + fileDiffMatch.Length)..nextMatch]; fileDiffChunks.Add((fileDiffMetadata, fileDiff)); }