From 1eb0f60d650c0d30f8766918af71d6140b9a2c3a Mon Sep 17 00:00:00 2001 From: Kevin BEAUGRAND Date: Thu, 1 Feb 2024 11:31:42 +0100 Subject: [PATCH] Add code generation and code interpretation sample code --- .gitignore | 1 + SemanticKernel.Assistants.sln | 9 + samples/01-mathematician/Program.cs | 21 +-- samples/02-autogen/02-autogen.csproj | 47 +++++ .../02-autogen/Assistants/AssistantAgent.yaml | 27 +++ .../Assistants/CodeInterpreter.yaml | 56 ++++++ .../Exceptions/CodeInterpreterException.cs | 19 ++ .../Plugins/CodeInterpretionPlugin.cs | 175 ++++++++++++++++++ .../Plugins/CodeInterpretionPluginOptions.cs | 15 ++ samples/02-autogen/Program.cs | 84 +++++++++ samples/02-autogen/appsettings.json | 5 + src/Assistants.Tests/HarnessTests.cs | 35 ++-- src/Assistants/Assistant.cs | 5 +- src/Assistants/AssistantBuilder.cs | 49 +---- src/Assistants/Extensions/KernelExtensions.cs | 35 ++-- src/Assistants/IAssistant.cs | 2 +- src/Assistants/IThread.cs | 6 + .../Models/AssistantExecutionSettings.cs | 3 + .../Models/AssistantInputParameters.cs | 40 +++- src/Assistants/Models/AssistantModel.cs | 6 +- src/Assistants/Thread.cs | 33 +++- 21 files changed, 570 insertions(+), 103 deletions(-) create mode 100644 samples/02-autogen/02-autogen.csproj create mode 100644 samples/02-autogen/Assistants/AssistantAgent.yaml create mode 100644 samples/02-autogen/Assistants/CodeInterpreter.yaml create mode 100644 samples/02-autogen/Exceptions/CodeInterpreterException.cs create mode 100644 samples/02-autogen/Plugins/CodeInterpretionPlugin.cs create mode 100644 samples/02-autogen/Plugins/CodeInterpretionPluginOptions.cs create mode 100644 samples/02-autogen/Program.cs create mode 100644 samples/02-autogen/appsettings.json diff --git a/.gitignore b/.gitignore index 7700772..47e0486 100644 --- a/.gitignore +++ b/.gitignore @@ -398,3 +398,4 @@ FodyWeavers.xsd *.sln.iml /src/Assistants.Tests/testsettings.development.json /samples/01-mathematician/appsettings.development.json +/samples/02-autogen/appsettings.development.json diff --git a/SemanticKernel.Assistants.sln b/SemanticKernel.Assistants.sln index baaa0e7..08624c8 100644 --- a/SemanticKernel.Assistants.sln +++ b/SemanticKernel.Assistants.sln @@ -32,6 +32,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\publish.yml = .github\workflows\publish.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "02-autogen", "samples\02-autogen\02-autogen.csproj", "{F5C5DEF1-3AB7-4DAF-A29D-A46483875287}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +59,12 @@ Global {BBC6C36F-DC43-4FD3-9706-ECA4738F8F57}.Publish|Any CPU.Build.0 = Release|Any CPU {BBC6C36F-DC43-4FD3-9706-ECA4738F8F57}.Release|Any CPU.ActiveCfg = Release|Any CPU {BBC6C36F-DC43-4FD3-9706-ECA4738F8F57}.Release|Any CPU.Build.0 = Release|Any CPU + {F5C5DEF1-3AB7-4DAF-A29D-A46483875287}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5C5DEF1-3AB7-4DAF-A29D-A46483875287}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5C5DEF1-3AB7-4DAF-A29D-A46483875287}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {F5C5DEF1-3AB7-4DAF-A29D-A46483875287}.Publish|Any CPU.Build.0 = Debug|Any CPU + {F5C5DEF1-3AB7-4DAF-A29D-A46483875287}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5C5DEF1-3AB7-4DAF-A29D-A46483875287}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -66,6 +74,7 @@ Global {03C21161-E835-4857-A81A-C1727140E920} = {96B59E8F-BF38-4918-8312-63DA3363B20B} {BBC6C36F-DC43-4FD3-9706-ECA4738F8F57} = {803BA424-8745-4689-9C1D-72CA4384E6AC} {B69AC62F-2CB1-4BCD-AB94-2C558AD3DB29} = {324300B5-4DBA-4DF0-957C-75458CCF93CE} + {F5C5DEF1-3AB7-4DAF-A29D-A46483875287} = {803BA424-8745-4689-9C1D-72CA4384E6AC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3252A8D5-644E-45F0-B096-AC8C2F0A15B4} diff --git a/samples/01-mathematician/Program.cs b/samples/01-mathematician/Program.cs index 25ea351..8917a1c 100644 --- a/samples/01-mathematician/Program.cs +++ b/samples/01-mathematician/Program.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using SemanticKernel.Assistants; +using SemanticKernel.Assistants.Extensions; using Spectre.Console; var configuration = new ConfigurationBuilder() @@ -39,21 +40,19 @@ .AddAzureOpenAIChatCompletion(azureOpenAIDeploymentName, azureOpenAIEndpoint, azureOpenAIKey) .Build(); - financialKernel.CreatePluginFromObject(new FinancialPlugin(), "financial"); + financialKernel.ImportPluginFromObject(new FinancialPlugin(), "financial"); + + var financialCalculator = AssistantBuilder.FromTemplate("./Assistants/FinancialCalculator.yaml") + .WithKernel(financialKernel) + .Build(); var butlerKernel = Kernel.CreateBuilder() - .AddAzureOpenAIChatCompletion(azureOpenAIDeploymentName, azureOpenAIEndpoint, azureOpenAIKey) - .Build(); + .AddAzureOpenAIChatCompletion(azureOpenAIDeploymentName, azureOpenAIEndpoint, azureOpenAIKey) + .Build(); - var financialCalculator = AssistantBuilder.FromTemplate("./Assistants/FinancialCalculator.yaml") - .WithKernel(financialKernel) - .Build(); + butlerKernel.ImportPluginFromAssistant(financialCalculator); - assistant = AssistantBuilder.FromTemplate("./Assistants/Butler.yaml", - assistants: new IAssistant[] - { - financialCalculator - }) + assistant = AssistantBuilder.FromTemplate("./Assistants/Butler.yaml") .WithKernel(butlerKernel) .Build(); }); diff --git a/samples/02-autogen/02-autogen.csproj b/samples/02-autogen/02-autogen.csproj new file mode 100644 index 0000000..628e105 --- /dev/null +++ b/samples/02-autogen/02-autogen.csproj @@ -0,0 +1,47 @@ + + + + Exe + net6.0 + _02_autogen + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + diff --git a/samples/02-autogen/Assistants/AssistantAgent.yaml b/samples/02-autogen/Assistants/AssistantAgent.yaml new file mode 100644 index 0000000..74ae724 --- /dev/null +++ b/samples/02-autogen/Assistants/AssistantAgent.yaml @@ -0,0 +1,27 @@ +name: AssistantAgent +description: A helpful and general-purpose AI assistant that has strong language skills, Python skills, and Linux command line skills. +instructions: | + You are a helpful and general-purpose AI assistant that has strong Python skills. + Solve tasks using your coding and language skills. + + In the following cases, suggest python code (in a python coding block) or shell script (in a sh coding block) for the CodeInterpreter to execute. + 1. When you need to collect info, use the code to output the info you need, for example, browse or search the web, download/read a file, print the content of a webpage or a file, get the current date/time, check the operating system. After sufficient info is printed and the task is ready to be solved based on your language skill, you can solve the task by yourself. + 2. When you need to perform some task with code, use the code to perform the task and output the result. Finish the task smartly. + 3. You should always choose the most precise way of solving the task. + + Solve the task step by step if you need to. If a plan is not provided, explain your plan first. Be clear which step uses code, and which step uses your language skill. + + When using code, you must indicate the script type in the code block. The user cannot provide any other feedback or perform any other action beyond executing the code you suggest. The user can't modify your code. So do not suggest incomplete code which requires users to modify. Don't use a code block if it's not intended to be executed by the user. + + If the result indicates there is an error, fix the error and output the code again. Suggest the full code instead of partial code or code changes. If the error can't be fixed or if the task is not solved even after the code is executed successfully, analyze the problem, revisit your assumption, collect additional info you need, and think of a different approach to try. + + Don't ever assume that you can't access the WEB, using CodeInterpreter agent, you'll be able to connect to the Internet. + + When you find an answer, verify the answer carefully. Include verifiable evidence in your response if possible. + Reply "TERMINATE" in the end when everything is done. +execution_settings: + planner: Stepwise + prompt_settings: + temperature: 0.0 + top_p: 1 + max_tokens: 1000 \ No newline at end of file diff --git a/samples/02-autogen/Assistants/CodeInterpreter.yaml b/samples/02-autogen/Assistants/CodeInterpreter.yaml new file mode 100644 index 0000000..ece435c --- /dev/null +++ b/samples/02-autogen/Assistants/CodeInterpreter.yaml @@ -0,0 +1,56 @@ +name: CodeInterpreter +description: | + This AI agent is designed to execute python code reliably and efficiently. + It has a built-in python interpreter that allows it to read, analyze and execute any valid python script. + This agent does not store instructions between calls, so you need to provide a complete code file to execute. +instructions: +input_parameters: + - name: input + is_required: True + default_value: "" + description: | + The python code to execute. + Make sure you follow the correct Python code syntax before submitting it. + + If you expect me to send you a result, you should use ``print`` method to output your expectactions. + + ## Example + ``` + x = 1 + y = 2.8 + z = 1j + + print(type(x)) + print(type(y)) + print(type(z)) + ``` + + This code should be sended like this: + ``` + x = 1\r\ny = 2.8\r\nz = 1j\r\n\r\nprint(type(x))\r\nprint(type(y))\r\nprint(type(z)) + ``` + + Important: You should alway send a full code to execute. + - name: requirements + is_required: False + default_value: "" + description: | + The contents of the Python requirements file to be used. + These requirements will be added to the ``requirements.txt`` file in the sandbox by the CodeInterpreter. + Here you must provide all the requirements necessary for the successful execution of your code. + + ## Example + ``` + tensorflow==2.3.1 + uvicorn==0.12.2 + fastapi==0.63.0 + ``` + +execution_settings: + planner: Handlebars + fixed_plan: | + {{json (code-ExecutePythonCode)}} + prompt_settings: + temperature: 0.0 + top_p: 1 + max_tokens: 1000 \ No newline at end of file diff --git a/samples/02-autogen/Exceptions/CodeInterpreterException.cs b/samples/02-autogen/Exceptions/CodeInterpreterException.cs new file mode 100644 index 0000000..c3ad54c --- /dev/null +++ b/samples/02-autogen/Exceptions/CodeInterpreterException.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace _02_autogen.Exceptions +{ + public class CodeInterpreterException : Exception + { + public CodeInterpreterException(string message, params string[] warnings) + : base(message) + { + this.Warnings = warnings; + } + + public string[] Warnings { get; } + } +} diff --git a/samples/02-autogen/Plugins/CodeInterpretionPlugin.cs b/samples/02-autogen/Plugins/CodeInterpretionPlugin.cs new file mode 100644 index 0000000..7282da2 --- /dev/null +++ b/samples/02-autogen/Plugins/CodeInterpretionPlugin.cs @@ -0,0 +1,175 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using _02_autogen.Exceptions; +using Docker.DotNet; +using Docker.DotNet.Models; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using System.ComponentModel; + +namespace _02_autogen.Plugins +{ + internal class CodeInterpretionPlugin + { + private readonly DockerClient _dockerClient; + + private readonly CodeInterpretionPluginOptions _options; + + private readonly ILogger _logger; + + private const string CodeFilePath = "/var/app/code.py"; + private const string RequirementsFilePath = "/var/app/requirements.txt"; + + public CodeInterpretionPlugin(CodeInterpretionPluginOptions options, ILoggerFactory loggerFactory) + { + this._options = options; + this._dockerClient = new DockerClientConfiguration(new Uri(options.DockerEndpoint)).CreateClient(); + + this._logger = loggerFactory.CreateLogger(); + } + + [KernelFunction] + [Description("Executes the specified python code in a sandbox.")] + [return: Description("The result of the program execution.")] + public async Task ExecutePythonCode(KernelArguments arguments = null!) + { + if (arguments == null) + { + throw new ArgumentNullException(nameof(arguments)); + } + + await PullRequiredImageAsync().ConfigureAwait(false); + + var instanceId = string.Empty; + + var codeFilePath = Path.GetTempFileName(); + var requirementsFilePath = Path.GetTempFileName(); + + try + { + if (arguments.TryGetValue("input", out var pythonCode)) + { + File.WriteAllText(codeFilePath, pythonCode!.ToString()); + } + else + { + throw new CodeInterpreterException("The input code is not correctly provided."); + } + + if (arguments.TryGetValue("requirements", out object? requirements)) + { + File.WriteAllText(requirementsFilePath, requirements?.ToString()); + } + + instanceId = await this.StartNewSandbox(@requirementsFilePath, codeFilePath).ConfigureAwait(false); + + this._logger.LogTrace($"Preparing Sandbox ({instanceId}:{Environment.NewLine}requirements.txt:{Environment.NewLine}{requirements}{Environment.NewLine}code.py:{Environment.NewLine}{pythonCode}"); + + await this.InstallRequirementsAsync(instanceId).ConfigureAwait(false); + + return await this.ExecuteCodeAsync(instanceId).ConfigureAwait(false); + } + finally + { + if (!string.IsNullOrEmpty(instanceId)) + { + await this._dockerClient.Containers.RemoveContainerAsync(instanceId, new ContainerRemoveParameters + { + Force = true + }).ConfigureAwait(false); + } + + if (File.Exists(codeFilePath)) + { + File.Delete(codeFilePath); + } + if (File.Exists(requirementsFilePath)) + { + File.Delete(requirementsFilePath); + } + } + } + + private async Task StartNewSandbox(string requirementFilePath, string codeFilePath) + { + var config = new Config() + { + Hostname = "localhost", + }; + + var containerCreateOptions = new CreateContainerParameters(config) + { + Image = this._options.DockerImage, + Entrypoint = new[] { "/bin/sh" }, + Tty = true, + NetworkDisabled = false, + HostConfig = new HostConfig() + { + Binds = new[] + { + $"{codeFilePath}:{CodeFilePath}", + $"{requirementFilePath}:{RequirementsFilePath}" + } + } + }; + + this._logger.LogDebug("Creating container."); + var response = await _dockerClient.Containers.CreateContainerAsync(containerCreateOptions).ConfigureAwait(false); + + this._logger.LogDebug($"Starting the container (id: {response.ID})."); + await _dockerClient.Containers.StartContainerAsync(response.ID, new ContainerStartParameters()).ConfigureAwait(false); + + return response.ID; + } + + private async Task InstallRequirementsAsync(string containerId) + { + _ = await this.ExecuteInContainer(containerId, $"pip install -r {RequirementsFilePath}"); + } + + private async Task ExecuteCodeAsync(string containerId) + { + return await this.ExecuteInContainer(containerId, $"python {CodeFilePath}").ConfigureAwait(false); + } + + private async Task ExecuteInContainer(string containerId, string command) + { + this._logger.LogDebug($"({containerId})# {command}"); + + var execContainer = await this._dockerClient.Exec.ExecCreateContainerAsync(containerId, new ContainerExecCreateParameters + { + AttachStderr = true, + AttachStdout = true, + AttachStdin = true, + Cmd = command.Split(' ', StringSplitOptions.RemoveEmptyEntries), + Tty = true + }).ConfigureAwait(false); + + var multiplexedStream = await _dockerClient.Exec.StartAndAttachContainerExecAsync(execContainer.ID, true); + + var output = await multiplexedStream.ReadOutputToEndAsync(CancellationToken.None); + + if (!string.IsNullOrWhiteSpace(output.stderr)) + { + this._logger.LogError($"({containerId}): {output.stderr}"); + throw new CodeInterpreterException(output.stderr); + } + + this._logger.LogDebug($"({containerId}): {output.stdout}"); + + return output.stdout; + } + + private async Task PullRequiredImageAsync() + { + try + { + _ = await _dockerClient.Images.InspectImageAsync(this._options.DockerImage); + } + catch (DockerImageNotFoundException) + { + await _dockerClient.Images.CreateImageAsync(new ImagesCreateParameters() { FromImage = this._options.DockerImage }, new AuthConfig(), new Progress()); + } + } + } +} diff --git a/samples/02-autogen/Plugins/CodeInterpretionPluginOptions.cs b/samples/02-autogen/Plugins/CodeInterpretionPluginOptions.cs new file mode 100644 index 0000000..7c2787c --- /dev/null +++ b/samples/02-autogen/Plugins/CodeInterpretionPluginOptions.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace _02_autogen.Plugins +{ + internal class CodeInterpretionPluginOptions + { + public string DockerEndpoint { get; set; } = string.Empty; + + public string DockerImage { get; set; } = "python:3-alpine"; + } +} diff --git a/samples/02-autogen/Program.cs b/samples/02-autogen/Program.cs new file mode 100644 index 0000000..2077484 --- /dev/null +++ b/samples/02-autogen/Program.cs @@ -0,0 +1,84 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using _02_autogen.Plugins; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using SemanticKernel.Assistants; +using SemanticKernel.Assistants.Extensions; +using Spectre.Console; + +var configuration = new ConfigurationBuilder() + .AddEnvironmentVariables() + .AddJsonFile("appsettings.json") + .AddJsonFile("appsettings.development.json", optional: true) + .Build(); + +using var loggerFactory = LoggerFactory.Create(logging => +{ + logging + .AddDebug() + .AddConfiguration(configuration.GetSection("Logging")); +}); + +AnsiConsole.Write(new FigletText($"Auto-Gen").Color(Color.Green)); +AnsiConsole.WriteLine(""); + +IAssistant assistant = null!; + +AnsiConsole.Status().Start("Initializing...", ctx => +{ + string azureOpenAIEndpoint = configuration["AzureOpenAIEndpoint"]!; + string azureOpenAIGPT4DeploymentName = configuration["AzureOpenAIGPT4DeploymentName"]!; + string azureOpenAIGPT35Endpoint = configuration["AzureOpenAIGPT35Endpoint"]!; + string azureOpenAIKey = configuration["AzureOpenAIAPIKey"]!; + string ollamaEndpoint = configuration["OllamaEndpoint"]!; + + var butlerKernel = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(azureOpenAIGPT4DeploymentName, azureOpenAIEndpoint, azureOpenAIKey) + .Build(); + + butlerKernel.ImportPluginFromAssistant(CreateCodeInterpreter(azureOpenAIGPT35Endpoint, azureOpenAIEndpoint, azureOpenAIKey)); + + assistant = AssistantBuilder.FromTemplate("./Assistants/AssistantAgent.yaml") + .WithKernel(butlerKernel) + .Build(); +}); + +var options = configuration.GetRequiredSection("CodeInterpreter"); + +var thread = assistant.CreateThread(); + +while (true) +{ + var prompt = AnsiConsole.Prompt(new TextPrompt("User > ").PromptStyle("teal")); + + AnsiConsole.MarkupLine($"[teal]User > {prompt}\n[/]"); + + await AnsiConsole.Status().StartAsync("Creating...", async ctx => + { + ctx.Spinner(Spinner.Known.Star); + ctx.SpinnerStyle(Style.Parse("green")); + ctx.Status($"Processing ..."); + + var answer = await thread.InvokeAsync(prompt).ConfigureAwait(true); + + AnsiConsole.MarkupLine($"[cyan]AutoGen > {answer.Content!}\n[/]"); + }); +} + +IAssistant CreateCodeInterpreter(string azureOpenAIDeploymentName, string azureOpenAIEndpoint, string azureOpenAIKey) +{ + var kernel = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(azureOpenAIDeploymentName, azureOpenAIEndpoint, azureOpenAIKey) + .Build(); + + var codeInterpretionOptions = new CodeInterpretionPluginOptions(); + configuration!.Bind("CodeInterpreter", codeInterpretionOptions); + + kernel.ImportPluginFromObject(new CodeInterpretionPlugin(codeInterpretionOptions, loggerFactory), "code"); + + return AssistantBuilder.FromTemplate("./Assistants/CodeInterpreter.yaml") + .WithKernel(kernel) + .Build(); +} \ No newline at end of file diff --git a/samples/02-autogen/appsettings.json b/samples/02-autogen/appsettings.json new file mode 100644 index 0000000..fd69011 --- /dev/null +++ b/samples/02-autogen/appsettings.json @@ -0,0 +1,5 @@ +{ + "AzureOpenAIEndpoint": "", + "AzureOpenAIAPIKey": "", + "OllamaEndpoint": "" +} diff --git a/src/Assistants.Tests/HarnessTests.cs b/src/Assistants.Tests/HarnessTests.cs index ed6a18a..4479c2b 100644 --- a/src/Assistants.Tests/HarnessTests.cs +++ b/src/Assistants.Tests/HarnessTests.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using SemanticKernel.Assistants; +using SemanticKernel.Assistants.Extensions; using SemanticKernel.Assistants.Tests.Plugins; using System.Threading.Tasks; using Xunit.Abstractions; @@ -103,18 +104,25 @@ public async Task ButlerTestAsync() "No need to show your work, just give the answer to the math problem.\n" + "Use calculation results.") .WithKernel(mathKernel) - .WithInputParameter("The word mathematics problem to solve in 2-3 sentences.\n" + - "Make sure to include all the input variables needed along with their values and units otherwise the math function will not be able to solve it.") + .WithInputParameters(new Assistants.Models.AssistantInputParameter + { + Name = "input", + Description = "The word mathematics problem to solve in 2-3 sentences.\n" + + "Make sure to include all the input variables needed along with their values and units otherwise the math function will not be able to solve it." + }) .Build(); + var butlerKernel = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(azureOpenAIChatCompletionDeployment, azureOpenAIEndpoint, azureOpenAIKey) + .Build(); + + butlerKernel.ImportPluginFromAssistant(mathematician); + var butler = new AssistantBuilder() .WithName("alfred") .WithDescription("An AI butler that helps humans.") .WithInstructions("Act as a butler.\nNo need to explain further the internal process.\nBe concise when answering.") - .WithKernel(Kernel.CreateBuilder() - .AddAzureOpenAIChatCompletion(azureOpenAIChatCompletionDeployment, azureOpenAIEndpoint, azureOpenAIKey) - .Build()) - .WithAssistant(mathematician) + .WithKernel(butlerKernel) .Build(); var thread = butler.CreateThread(); @@ -143,14 +151,15 @@ public async Task FinancialAdvisorFromTemplateTestsAsync() .WithKernel(financialKernel) .Build(); - var butler = AssistantBuilder.FromTemplate("./Assistants/Butler.yaml", - assistants: new[] { - mathematician, - financial - }) - .WithKernel(Kernel.CreateBuilder() + var butlerKernel = Kernel.CreateBuilder() .AddAzureOpenAIChatCompletion(azureOpenAIChatCompletionDeployment, azureOpenAIEndpoint, azureOpenAIKey) - .Build()) + .Build(); + + butlerKernel.ImportPluginFromAssistant(mathematician); + butlerKernel.ImportPluginFromAssistant(financial); + + var butler = AssistantBuilder.FromTemplate("./Assistants/Butler.yaml") + .WithKernel(butlerKernel) .Build(); var thread = butler.CreateThread(); diff --git a/src/Assistants/Assistant.cs b/src/Assistants/Assistant.cs index 46217b5..f7f5827 100644 --- a/src/Assistants/Assistant.cs +++ b/src/Assistants/Assistant.cs @@ -103,11 +103,10 @@ public IThread CreateThread() /// /// Create a new conversable thread using actual kernel arguments. /// - /// The agent that is creating a thread with this agent. /// The actual kernel parameters. /// - IThread IAssistant.CreateThread(IAssistant initatedAgent, Dictionary arguments) + IThread IAssistant.CreateThread(Dictionary arguments) { - return new Thread(this, initatedAgent.Name!, arguments); + return new Thread(this, arguments); } } diff --git a/src/Assistants/AssistantBuilder.cs b/src/Assistants/AssistantBuilder.cs index f79076e..14e22c4 100644 --- a/src/Assistants/AssistantBuilder.cs +++ b/src/Assistants/AssistantBuilder.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using YamlDotNet.Serialization; namespace SemanticKernel.Assistants; @@ -18,11 +19,6 @@ namespace SemanticKernel.Assistants; /// public partial class AssistantBuilder { - /// - /// The agent's assistants. - /// - private readonly List _assistants; - /// /// The kernel builder. /// @@ -39,7 +35,6 @@ public partial class AssistantBuilder public AssistantBuilder() { this.Model = new AssistantModel(); - this._assistants = new List(); } /// @@ -56,11 +51,6 @@ public IAssistant Build() var agent = new Assistant(this.Model, this.Kernel); - foreach (var item in this._assistants) - { - this.Kernel.ImportPluginFromAgent(agent, item); - } - return agent; } @@ -97,18 +87,6 @@ public AssistantBuilder WithInstructions(string instructions) return this; } - /// - /// Adds the agent's collaborative assistant. - /// - /// The assistant. - /// - public AssistantBuilder WithAssistant(IAssistant assistant) - { - this._assistants.Add(assistant); - - return this; - } - /// /// Defines the agent's planner. /// @@ -133,17 +111,14 @@ public AssistantBuilder WithExecutionSettings(AssistantPromptExecutionSettings e } /// - /// Defines the agent's input parameter. + /// Configures the input parameters for the assistant. /// - /// The input parameter. + /// The input parameters. /// - public AssistantBuilder WithInputParameter(string description, string defaultValue = "") + public AssistantBuilder WithInputParameters(params AssistantInputParameter[] parameters) { - this.Model.Input = new AssistantInputParameter - { - DefaultValue = defaultValue, - Description = description, - }; + this.Model.Inputs = parameters; + return this; } @@ -162,11 +137,9 @@ public AssistantBuilder WithKernel(Kernel kernel) /// Creates a new agent builder from a yaml template. /// /// The yaml definition file path. - /// The assistants. /// public static AssistantBuilder FromTemplate( - string definitionPath, - params IAssistant[] assistants) + string definitionPath) { var deserializer = new DeserializerBuilder().Build(); var yamlContent = File.ReadAllText(definitionPath); @@ -176,14 +149,6 @@ public static AssistantBuilder FromTemplate( var agentBuilder = new AssistantBuilder(); agentBuilder.Model = agentModel; - if (assistants is not null) - { - foreach (var assistant in assistants) - { - agentBuilder.WithAssistant(assistant); - } - } - return agentBuilder; } } diff --git a/src/Assistants/Extensions/KernelExtensions.cs b/src/Assistants/Extensions/KernelExtensions.cs index c5e9c6f..06c2182 100644 --- a/src/Assistants/Extensions/KernelExtensions.cs +++ b/src/Assistants/Extensions/KernelExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.SemanticKernel; using System; +using System.Diagnostics; using System.Linq; namespace SemanticKernel.Assistants.Extensions; @@ -9,40 +10,32 @@ namespace SemanticKernel.Assistants.Extensions; /// /// Extensions for . /// -internal static class KernelExtensions +public static class KernelExtensions { /// /// Imports the agent's plugin into the kernel. /// /// The Kernel instance. - /// The Agent to import. - /// The instance. - public static void ImportPluginFromAgent(this Kernel kernel, IAssistant agent, IAssistant otherAssistant) + /// The instance. + public static void ImportPluginFromAssistant(this Kernel kernel, IAssistant assistant) { - var agentConversationPlugin = KernelPluginFactory.CreateFromFunctions(otherAssistant.Name!, otherAssistant.Description!, functions: new[] + if (assistant is null) + { + throw new ArgumentNullException(nameof(assistant)); + } + + var agentConversationPlugin = KernelPluginFactory.CreateFromFunctions(assistant.Name!, assistant.Description!, functions: new[] { KernelFunctionFactory.CreateFromMethod(async (string input, KernelArguments args) => { - if (!agent.AssistantThreads.TryGetValue(otherAssistant, out var thread)) - { - thread = otherAssistant.CreateThread(agent, args.ToDictionary()); - agent.AssistantThreads.Add(otherAssistant, thread); - } + var thread = assistant.CreateThread( args.ToDictionary()); return await thread.InvokeAsync(input).ConfigureAwait(false); }, functionName: "Ask", - description: otherAssistant.Description, - parameters: new[] - { - new KernelParameterMetadata("input") - { - IsRequired = true, - ParameterType = typeof(string), - DefaultValue = otherAssistant.AssistantModel.Input.DefaultValue, - Description = otherAssistant.AssistantModel.Input.Description - } - }, returnParameter: new() + description: assistant.Description, + parameters: assistant.AssistantModel.Inputs.Select(c => c.ToKernelParameterMetadata()), + returnParameter: new() { ParameterType = typeof(string), Description = "The response from the assistant." diff --git a/src/Assistants/IAssistant.cs b/src/Assistants/IAssistant.cs index 3b9ab77..f457674 100644 --- a/src/Assistants/IAssistant.cs +++ b/src/Assistants/IAssistant.cs @@ -65,5 +65,5 @@ public interface IAssistant /// /// Create a new conversable thread using actual kernel arguments. /// - internal IThread CreateThread(IAssistant initatedAgent, Dictionary arguments); + internal IThread CreateThread(Dictionary arguments); } diff --git a/src/Assistants/IThread.cs b/src/Assistants/IThread.cs index 340ab1e..fb06434 100644 --- a/src/Assistants/IThread.cs +++ b/src/Assistants/IThread.cs @@ -27,4 +27,10 @@ public interface IThread /// Gets the chat messages. /// IReadOnlyList ChatMessages { get; } + + /// + /// Updates the kernel arguments. + /// + /// The new kernel arguments. + internal void UpdateKernelArguments(KernelArguments arguments); } diff --git a/src/Assistants/Models/AssistantExecutionSettings.cs b/src/Assistants/Models/AssistantExecutionSettings.cs index 6586d78..65f065e 100644 --- a/src/Assistants/Models/AssistantExecutionSettings.cs +++ b/src/Assistants/Models/AssistantExecutionSettings.cs @@ -9,6 +9,9 @@ internal class AssistantExecutionSettings [YamlMember(Alias = "planner")] public string? Planner { get; set; } + [YamlMember(Alias = "fixed_plan")] + public string? FixedPlan { get; set; } + [YamlMember(Alias = "past_messages_included")] public int PastMessagesIncluded { get; set; } = 10; diff --git a/src/Assistants/Models/AssistantInputParameters.cs b/src/Assistants/Models/AssistantInputParameters.cs index 0fc03d2..df592a4 100644 --- a/src/Assistants/Models/AssistantInputParameters.cs +++ b/src/Assistants/Models/AssistantInputParameters.cs @@ -1,14 +1,52 @@ // Copyright (c) Kevin BEAUGRAND. All rights reserved. +using Microsoft.SemanticKernel; +using System; using YamlDotNet.Serialization; namespace SemanticKernel.Assistants.Models; -internal class AssistantInputParameter +/// +/// Class representing the assistant input parameter. +/// +public class AssistantInputParameter { + /// + /// Gets or sets the input name. + /// + [YamlMember(Alias = "name")] + public string? Name { get; set; } + + /// + /// Gets or sets the parameter description. + /// [YamlMember(Alias = "description")] public string? Description { get; set; } [YamlMember(Alias = "default_value")] public string? DefaultValue { get; set; } + + /// Gets whether the parameter is required. + [YamlMember(Alias = "is_required")] + public bool IsRequired { get; set; } + + /// Gets the .NET type of the parameter. + [YamlMember(Alias = "parameter_type")] + public Type? ParameterType { get; set; } + + /// Gets a JSON Schema describing the parameter's type. + [YamlMember(Alias = "schema")] + public KernelJsonSchema? Schema { get; set; } + + internal KernelParameterMetadata ToKernelParameterMetadata() + { + return new KernelParameterMetadata(this.Name!) + { + DefaultValue = this.DefaultValue, + Description = this.Description, + IsRequired = this.IsRequired, + ParameterType = this.ParameterType, + Schema = this.Schema + }; + } } diff --git a/src/Assistants/Models/AssistantModel.cs b/src/Assistants/Models/AssistantModel.cs index 479f3e5..7d055ee 100644 --- a/src/Assistants/Models/AssistantModel.cs +++ b/src/Assistants/Models/AssistantModel.cs @@ -1,6 +1,8 @@ // Copyright (c) Kevin BEAUGRAND. All rights reserved. +using Microsoft.SemanticKernel; using SemanticKernel.Assistants.Models; +using System; using YamlDotNet.Serialization; namespace SemanticKernel.Assistants.Models; @@ -19,6 +21,6 @@ internal class AssistantModel [YamlMember(Alias = "execution_settings")] public AssistantExecutionSettings ExecutionSettings { get; set; } = new(); - [YamlMember(Alias = "input_parameter")] - public AssistantInputParameter Input { get; set; } = new(); + [YamlMember(Alias = "input_parameters")] + public AssistantInputParameter[] Inputs { get; set; } = Array.Empty(); } diff --git a/src/Assistants/Thread.cs b/src/Assistants/Thread.cs index f6d6136..3f3943f 100644 --- a/src/Assistants/Thread.cs +++ b/src/Assistants/Thread.cs @@ -6,6 +6,7 @@ using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Planning; using Microsoft.SemanticKernel.Planning.Handlebars; +using SemanticKernel.Assistants.Extensions; using System; using System.Collections.Generic; using System.Linq; @@ -49,7 +50,7 @@ public class Thread : IThread /// /// The arguments to pass to the agent. /// - private readonly Dictionary _arguments; + private Dictionary _arguments; /// /// The name of the caller. @@ -65,15 +66,13 @@ public class Thread : IThread /// Initializes a new instance of the class. /// /// The agent. - /// The caller name. /// The arguments to pass. internal Thread(IAssistant agent, - string callerName = "User", Dictionary arguments = null) { this._logger = agent.Kernel.LoggerFactory.CreateLogger(); this._agent = agent; - this._callerName = callerName; + this._callerName = "User"; this._arguments = arguments ?? new Dictionary(); this._chatHistory = new ChatHistory(this._agent.Description!); @@ -86,7 +85,7 @@ internal Thread(IAssistant agent, FrequencyPenalty = this._agent.AssistantModel.ExecutionSettings.PromptExecutionSettings.FrequencyPenalty, PresencePenalty = this._agent.AssistantModel.ExecutionSettings.PromptExecutionSettings.PresencePenalty, MaxTokens = this._agent.AssistantModel.ExecutionSettings.PromptExecutionSettings.MaxTokens, - StopSequences = this._agent.AssistantModel.ExecutionSettings.PromptExecutionSettings.StopSequences + StopSequences = this._agent.AssistantModel.ExecutionSettings.PromptExecutionSettings.StopSequences }; } @@ -107,7 +106,7 @@ public async Task InvokeAsync(string userMessage) else { assistantAnswer = new ChatMessageContent(AuthorRole.Assistant, await this.GetChatAnswer(userMessage).ConfigureAwait(false)); - } + } this._chatHistory.AddUserMessage(userMessage); this._chatHistory.Add(assistantAnswer); @@ -207,7 +206,9 @@ private async Task ExecutePlannerAsync(string userIntent) var goal = $"{this._agent.Instructions}\n" + $"Given the following context, accomplish the user intent.\n" + $"## User intent\n" + - $"{userIntent}"; + $"{userIntent}\n" + + $"## Current parameters\n" + + $"{string.Join(Environment.NewLine, this._arguments.Select(c => $"{c.Key}:\n{c.Value}"))}"; switch (this._agent.Planner.ToLower()) { @@ -235,8 +236,17 @@ private async Task ExecuteHandleBarsPlannerAsync(string goal, int maxTri LastError = lastError?.Message // Pass in the last error to avoid trying the same thing again }); - var plan = await planner.CreatePlanAsync(this._agent.Kernel, goal).ConfigureAwait(false); - lastPlan = plan; + HandlebarsPlan plan = null!; + + if (!string.IsNullOrEmpty(this._agent.AssistantModel.ExecutionSettings.FixedPlan)) + { + plan = new HandlebarsPlan(this._agent.AssistantModel.ExecutionSettings.FixedPlan!); + } + else + { + plan = await planner.CreatePlanAsync(this._agent.Kernel, goal).ConfigureAwait(false); + lastPlan = plan; + } this._logger.LogDebug($"Plan: {plan}"); @@ -273,4 +283,9 @@ private async Task ExecuteStepwisePlannerAsync(string goal) return result.FinalAnswer!.Trim(); } + + void IThread.UpdateKernelArguments(KernelArguments arguments) + { + this._arguments = arguments.ToDictionary(); ; + } }