diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 6b7056cce6dc..1b53d0bde88f 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -52,11 +52,24 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] + python-version: ["3.11"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 with: lfs: true + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install jupyter and ipykernel + run: | + python -m pip install --upgrade pip + python -m pip install jupyter + python -m pip install ipykernel + - name: list available kernels + run: | + python -m jupyter kernelspec list - name: Setup .NET uses: actions/setup-dotnet@v4 with: @@ -114,6 +127,18 @@ jobs: - uses: actions/checkout@v4 with: lfs: true + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: 3.11 + - name: Install jupyter and ipykernel + run: | + python -m pip install --upgrade pip + python -m pip install jupyter + python -m pip install ipykernel + - name: list available kernels + run: | + python -m jupyter kernelspec list - name: Setup .NET uses: actions/setup-dotnet@v4 with: diff --git a/.github/workflows/dotnet-release.yml b/.github/workflows/dotnet-release.yml index aacfd115bb7e..23f4258a0e0c 100644 --- a/.github/workflows/dotnet-release.yml +++ b/.github/workflows/dotnet-release.yml @@ -29,6 +29,18 @@ jobs: - uses: actions/checkout@v4 with: lfs: true + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: 3.11 + - name: Install jupyter and ipykernel + run: | + python -m pip install --upgrade pip + python -m pip install jupyter + python -m pip install ipykernel + - name: list available kernels + run: | + python -m jupyter kernelspec list - name: Setup .NET uses: actions/setup-dotnet@v4 with: diff --git a/dotnet/eng/Version.props b/dotnet/eng/Version.props index 20be183219e5..c78ce4b415fc 100644 --- a/dotnet/eng/Version.props +++ b/dotnet/eng/Version.props @@ -15,5 +15,6 @@ 8.0.4 3.0.0 4.3.0.2 + 7.4.4 \ No newline at end of file diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs index e498650b6aac..a1e110bcc6a5 100644 --- a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs @@ -4,6 +4,7 @@ #region code_snippet_0_1 using AutoGen.Core; using AutoGen.DotnetInteractive; +using AutoGen.DotnetInteractive.Extension; #endregion code_snippet_0_1 namespace AutoGen.BasicSample.CodeSnippet; @@ -11,18 +12,37 @@ public class RunCodeSnippetCodeSnippet { public async Task CodeSnippet1() { - IAgent agent = default; + IAgent agent = new DefaultReplyAgent("agent", "Hello World"); #region code_snippet_1_1 - var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - Directory.CreateDirectory(workingDirectory); - var interactiveService = new InteractiveService(installingDirectory: workingDirectory); - await interactiveService.StartAsync(workingDirectory: workingDirectory); + var kernel = DotnetInteractiveKernelBuilder + .CreateDefaultBuilder() // add C# and F# kernels + .Build(); #endregion code_snippet_1_1 #region code_snippet_1_2 - // register dotnet code block execution hook to an arbitrary agent - var dotnetCodeAgent = agent.RegisterDotnetCodeBlockExectionHook(interactiveService: interactiveService); + // register middleware to execute code block + var dotnetCodeAgent = agent + .RegisterMiddleware(async (msgs, option, innerAgent, ct) => + { + var lastMessage = msgs.LastOrDefault(); + if (lastMessage == null || lastMessage.GetContent() is null) + { + return await innerAgent.GenerateReplyAsync(msgs, option, ct); + } + + if (lastMessage.ExtractCodeBlock("```csharp", "```") is string codeSnippet) + { + // execute code snippet + var result = await kernel.RunSubmitCodeCommandAsync(codeSnippet, "csharp"); + return new TextMessage(Role.Assistant, result, from: agent.Name); + } + else + { + // no code block found, invoke next agent + return await innerAgent.GenerateReplyAsync(msgs, option, ct); + } + }); var codeSnippet = @" ```csharp @@ -44,5 +64,17 @@ public async Task CodeSnippet1() ``` "; #endregion code_snippet_1_3 + + #region code_snippet_1_4 + var pythonKernel = DotnetInteractiveKernelBuilder + .CreateDefaultBuilder() + .AddPythonKernel(venv: "python3") + .Build(); + + var pythonCode = """ + print('Hello from Python!') + """; + var result = await pythonKernel.RunSubmitCodeCommandAsync(pythonCode, "python3"); + #endregion code_snippet_1_4 } } diff --git a/dotnet/sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs b/dotnet/sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs index 216059928408..c9e8a0cab155 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs @@ -5,6 +5,7 @@ using AutoGen.BasicSample; using AutoGen.Core; using AutoGen.DotnetInteractive; +using AutoGen.DotnetInteractive.Extension; using AutoGen.OpenAI; using FluentAssertions; @@ -14,32 +15,13 @@ public static async Task RunAsync() { var instance = new Example04_Dynamic_GroupChat_Coding_Task(); - // setup dotnet interactive - var workDir = Path.Combine(Path.GetTempPath(), "InteractiveService"); - if (!Directory.Exists(workDir)) - { - Directory.CreateDirectory(workDir); - } - - using var service = new InteractiveService(workDir); - var dotnetInteractiveFunctions = new DotnetInteractiveFunction(service); - - var result = Path.Combine(workDir, "result.txt"); - if (File.Exists(result)) - { - File.Delete(result); - } - - await service.StartAsync(workDir, default); + var kernel = DotnetInteractiveKernelBuilder + .CreateDefaultBuilder() + .AddPythonKernel("python3") + .Build(); var gptConfig = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); - var helperAgent = new GPTAgent( - name: "helper", - systemMessage: "You are a helpful AI assistant", - temperature: 0f, - config: gptConfig); - var groupAdmin = new GPTAgent( name: "groupAdmin", systemMessage: "You are the admin of the group chat", @@ -56,8 +38,8 @@ public static async Task RunAsync() systemMessage: """ You are a manager who takes coding problem from user and resolve problem by splitting them into small tasks and assign each task to the most appropriate agent. Here's available agents who you can assign task to: - - coder: write dotnet code to resolve task - - runner: run dotnet code from coder + - coder: write python code to resolve task + - runner: run python code from coder The workflow is as follows: - You take the coding problem from user @@ -83,15 +65,7 @@ You are a manager who takes coding problem from user and resolve problem by spli Once the coding problem is resolved, summarize each steps and results and send the summary to the user using the following format: ```summary - { - "problem": "{coding problem}", - "steps": [ - { - "step": "{step}", - "result": "{result}" - } - ] - } + @user, ``` Your reply must contain one of [task|ask|summary] to indicate the type of your message. @@ -110,19 +84,16 @@ Your reply must contain one of [task|ask|summary] to indicate the type of your m // The nuget agent install nuget packages if there's any. var coderAgent = new GPTAgent( name: "coder", - systemMessage: @"You act as dotnet coder, you write dotnet code to resolve task. Once you finish writing code, ask runner to run the code for you. + systemMessage: @"You act as python coder, you write python code to resolve task. Once you finish writing code, ask runner to run the code for you. Here're some rules to follow on writing dotnet code: -- put code between ```csharp and ``` -- When creating http client, use `var httpClient = new HttpClient()`. Don't use `using var httpClient = new HttpClient()` because it will cause error when running the code. -- Try to use `var` instead of explicit type. -- Try avoid using external library, use .NET Core library instead. -- Use top level statement to write code. +- put code between ```python and ``` +- Try avoid using external library - Always print out the result to console. Don't write code that doesn't print out anything. -If you need to install nuget packages, put nuget packages in the following format: -```nuget -nuget_package_name +Use the following format to install pip package: +```python +%pip install ``` If your code is incorrect, Fix the error and send the code again. @@ -143,10 +114,8 @@ Your reply must contain one of [task|ask|summary] to indicate the type of your m name: "reviewer", systemMessage: """ You are a code reviewer who reviews code from coder. You need to check if the code satisfy the following conditions: - - The reply from coder contains at least one code block, e.g ```csharp and ``` - - There's only one code block and it's csharp code block - - The code block is not inside a main function. a.k.a top level statement - - The code block is not using declaration when creating http client + - The reply from coder contains at least one code block, e.g ```python and ``` + - There's only one code block and it's python code block You don't check the code style, only check if the code satisfy the above conditions. @@ -173,14 +142,32 @@ Your reply must contain one of [task|ask|summary] to indicate the type of your m // The runner agent will run the code block from coder's reply. // It runs dotnet code using dotnet interactive service hook. // It also truncate the output if the output is too long. - var runner = new AssistantAgent( + var runner = new DefaultReplyAgent( name: "runner", defaultReply: "No code available, coder, write code please") - .RegisterDotnetCodeBlockExectionHook(interactiveService: service) .RegisterMiddleware(async (msgs, option, agent, ct) => { var mostRecentCoderMessage = msgs.LastOrDefault(x => x.From == "coder") ?? throw new Exception("No coder message found"); - return await agent.GenerateReplyAsync(new[] { mostRecentCoderMessage }, option, ct); + + if (mostRecentCoderMessage.ExtractCodeBlock("```python", "```") is string code) + { + var result = await kernel.RunSubmitCodeCommandAsync(code, "python"); + // only keep the first 500 characters + if (result.Length > 500) + { + result = result.Substring(0, 500); + } + result = $""" + # [CODE_BLOCK_EXECUTION_RESULT] + {result} + """; + + return new TextMessage(Role.Assistant, result, from: agent.Name); + } + else + { + return await agent.GenerateReplyAsync(msgs, option, ct); + } }) .RegisterPrintMessage(); @@ -251,18 +238,27 @@ Your reply must contain one of [task|ask|summary] to indicate the type of your m workflow: workflow); // task 1: retrieve the most recent pr from mlnet and save it in result.txt - var groupChatManager = new GroupChatManager(groupChat); - await userProxy.SendAsync(groupChatManager, "Retrieve the most recent pr from mlnet and save it in result.txt", maxRound: 30); - File.Exists(result).Should().BeTrue(); - - // task 2: calculate the 39th fibonacci number - var answer = 63245986; - // clear the result file - File.Delete(result); + var task = """ + retrieve the most recent pr from mlnet and save it in result.txt + """; + var chatHistory = new List + { + new TextMessage(Role.Assistant, task) + { + From = userProxy.Name + } + }; + await foreach (var message in groupChat.SendAsync(chatHistory, maxRound: 10)) + { + if (message.From == admin.Name && message.GetContent().Contains("```summary")) + { + // Task complete! + break; + } + } - var conversationHistory = await userProxy.InitiateChatAsync(groupChatManager, "What's the 39th of fibonacci number? Save the result in result.txt", maxRound: 10); + // check if the result file is created + var result = "result.txt"; File.Exists(result).Should().BeTrue(); - var resultContent = File.ReadAllText(result); - resultContent.Should().Contain(answer.ToString()); } } diff --git a/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs b/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs index dd4fcada9673..d78bb7656ae6 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs @@ -6,10 +6,11 @@ using AutoGen.BasicSample; using AutoGen.Core; using AutoGen.DotnetInteractive; +using AutoGen.DotnetInteractive.Extension; using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; using Azure.AI.OpenAI; -using FluentAssertions; +using Microsoft.DotNet.Interactive; public partial class Example07_Dynamic_GroupChat_Calculate_Fibonacci { @@ -80,12 +81,11 @@ public static async Task CreateCoderAgentAsync(OpenAIClient client, stri #endregion create_coder #region create_runner - public static async Task CreateRunnerAgentAsync(InteractiveService service) + public static async Task CreateRunnerAgentAsync(Kernel kernel) { var runner = new DefaultReplyAgent( name: "runner", defaultReply: "No code available.") - .RegisterDotnetCodeBlockExectionHook(interactiveService: service) .RegisterMiddleware(async (msgs, option, agent, _) => { if (msgs.Count() == 0 || msgs.All(msg => msg.From != "coder")) @@ -95,7 +95,24 @@ public static async Task CreateRunnerAgentAsync(InteractiveService servi else { var coderMsg = msgs.Last(msg => msg.From == "coder"); - return await agent.GenerateReplyAsync([coderMsg], option); + if (coderMsg.ExtractCodeBlock("```csharp", "```") is string code) + { + var codeResult = await kernel.RunSubmitCodeCommandAsync(code, "csharp"); + + codeResult = $""" + [RUNNER_RESULT] + {codeResult} + """; + + return new TextMessage(Role.Assistant, codeResult) + { + From = "runner", + }; + } + else + { + return new TextMessage(Role.Assistant, "No code available. Coder please write code"); + } } }) .RegisterPrintMessage(); @@ -216,24 +233,15 @@ public static async Task CreateReviewerAgentAsync(OpenAIClient openAICli public static async Task RunWorkflowAsync() { long the39thFibonacciNumber = 63245986; - var workDir = Path.Combine(Path.GetTempPath(), "InteractiveService"); - if (!Directory.Exists(workDir)) - { - Directory.CreateDirectory(workDir); - } + var kernel = DotnetInteractiveKernelBuilder.CreateDefaultBuilder().Build(); var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); var openaiClient = new OpenAIClient(new Uri(config.Endpoint), new Azure.AzureKeyCredential(config.ApiKey)); - using var service = new InteractiveService(workDir); - var dotnetInteractiveFunctions = new DotnetInteractiveFunction(service); - - await service.StartAsync(workDir, default); - #region create_workflow var reviewer = await CreateReviewerAgentAsync(openaiClient, config.DeploymentName); var coder = await CreateCoderAgentAsync(openaiClient, config.DeploymentName); - var runner = await CreateRunnerAgentAsync(service); + var runner = await CreateRunnerAgentAsync(kernel); var admin = await CreateAdminAsync(openaiClient, config.DeploymentName); var admin2CoderTransition = Transition.Create(admin, coder); @@ -305,21 +313,23 @@ public static async Task RunWorkflowAsync() runner, reviewer, ]); - + #endregion create_group_chat_with_workflow admin.SendIntroduction("Welcome to my group, work together to resolve my task", groupChat); coder.SendIntroduction("I will write dotnet code to resolve task", groupChat); reviewer.SendIntroduction("I will review dotnet code", groupChat); runner.SendIntroduction("I will run dotnet code once the review is done", groupChat); + var task = "What's the 39th of fibonacci number?"; - var groupChatManager = new GroupChatManager(groupChat); - var conversationHistory = await admin.InitiateChatAsync(groupChatManager, "What's the 39th of fibonacci number?", maxRound: 10); - #endregion create_group_chat_with_workflow - // the last message is from admin, which is the termination message - var lastMessage = conversationHistory.Last(); - lastMessage.From.Should().Be("admin"); - lastMessage.IsGroupChatTerminateMessage().Should().BeTrue(); - lastMessage.Should().BeOfType(); - lastMessage.GetContent().Should().Contain(the39thFibonacciNumber.ToString()); + var taskMessage = new TextMessage(Role.User, task, from: admin.Name); + await foreach (var message in groupChat.SendAsync([taskMessage], maxRound: 10)) + { + // teminate chat if message is from runner and run successfully + if (message.From == "runner" && message.GetContent().Contains(the39thFibonacciNumber.ToString())) + { + Console.WriteLine($"The 39th of fibonacci number is {the39thFibonacciNumber}"); + break; + } + } } public static async Task RunAsync() @@ -334,14 +344,11 @@ public static async Task RunAsync() var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); var openaiClient = new OpenAIClient(new Uri(config.Endpoint), new Azure.AzureKeyCredential(config.ApiKey)); - using var service = new InteractiveService(workDir); - var dotnetInteractiveFunctions = new DotnetInteractiveFunction(service); - - await service.StartAsync(workDir, default); + var kernel = DotnetInteractiveKernelBuilder.CreateDefaultBuilder().Build(); #region create_group_chat var reviewer = await CreateReviewerAgentAsync(openaiClient, config.DeploymentName); var coder = await CreateCoderAgentAsync(openaiClient, config.DeploymentName); - var runner = await CreateRunnerAgentAsync(service); + var runner = await CreateRunnerAgentAsync(kernel); var admin = await CreateAdminAsync(openaiClient, config.DeploymentName); var groupChat = new GroupChat( admin: admin, diff --git a/dotnet/sample/AutoGen.BasicSamples/Program.cs b/dotnet/sample/AutoGen.BasicSamples/Program.cs index b48e2be4aa16..a8bc7e1b015d 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Program.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Program.cs @@ -1,6 +1,4 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Program.cs -using AutoGen.BasicSample; -Console.ReadLine(); -await Example17_ReActAgent.RunAsync(); +await Example07_Dynamic_GroupChat_Calculate_Fibonacci.RunAsync(); diff --git a/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj b/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj index 5778761f05da..e850d94944bc 100644 --- a/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj +++ b/dotnet/src/AutoGen.DotnetInteractive/AutoGen.DotnetInteractive.csproj @@ -27,9 +27,14 @@ + + + + + - + diff --git a/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveKernelBuilder.cs b/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveKernelBuilder.cs new file mode 100644 index 000000000000..a8f330154922 --- /dev/null +++ b/dotnet/src/AutoGen.DotnetInteractive/DotnetInteractiveKernelBuilder.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// DotnetInteractiveKernelBuilder.cs + +#if NET8_0_OR_GREATER +using AutoGen.DotnetInteractive.Extension; +using Microsoft.DotNet.Interactive; +using Microsoft.DotNet.Interactive.Commands; +using Microsoft.DotNet.Interactive.CSharp; +using Microsoft.DotNet.Interactive.FSharp; +using Microsoft.DotNet.Interactive.Jupyter; +using Microsoft.DotNet.Interactive.PackageManagement; +using Microsoft.DotNet.Interactive.PowerShell; + +namespace AutoGen.DotnetInteractive; + +public class DotnetInteractiveKernelBuilder +{ + private readonly CompositeKernel compositeKernel; + + private DotnetInteractiveKernelBuilder() + { + this.compositeKernel = new CompositeKernel(); + + // add jupyter connector + this.compositeKernel.AddKernelConnector( + new ConnectJupyterKernelCommand() + .AddConnectionOptions(new JupyterHttpKernelConnectionOptions()) + .AddConnectionOptions(new JupyterLocalKernelConnectionOptions())); + } + + /// + /// Create an empty builder. + /// + /// + public static DotnetInteractiveKernelBuilder CreateEmptyBuilder() + { + return new DotnetInteractiveKernelBuilder(); + } + + /// + /// Create a default builder with C# and F# kernels. + /// + public static DotnetInteractiveKernelBuilder CreateDefaultBuilder() + { + return new DotnetInteractiveKernelBuilder() + .AddCSharpKernel() + .AddFSharpKernel(); + } + + public DotnetInteractiveKernelBuilder AddCSharpKernel(IEnumerable? aliases = null) + { + aliases ??= ["c#", "C#"]; + // create csharp kernel + var csharpKernel = new CSharpKernel() + .UseNugetDirective((k, resolvedPackageReference) => + { + + k.AddAssemblyReferences(resolvedPackageReference + .SelectMany(r => r.AssemblyPaths)); + return Task.CompletedTask; + }) + .UseKernelHelpers() + .UseWho() + .UseMathAndLaTeX() + .UseValueSharing(); + + this.AddKernel(csharpKernel, aliases); + + return this; + } + + public DotnetInteractiveKernelBuilder AddFSharpKernel(IEnumerable? aliases = null) + { + aliases ??= ["f#", "F#"]; + // create fsharp kernel + var fsharpKernel = new FSharpKernel() + .UseDefaultFormatting() + .UseKernelHelpers() + .UseWho() + .UseMathAndLaTeX() + .UseValueSharing(); + + this.AddKernel(fsharpKernel, aliases); + + return this; + } + + public DotnetInteractiveKernelBuilder AddPowershellKernel(IEnumerable? aliases = null) + { + aliases ??= ["pwsh", "powershell"]; + // create powershell kernel + var powershellKernel = new PowerShellKernel() + .UseProfiles() + .UseValueSharing(); + + this.AddKernel(powershellKernel, aliases); + + return this; + } + + public DotnetInteractiveKernelBuilder AddPythonKernel(string venv, string kernelName = "python", IEnumerable? aliases = null) + { + aliases ??= [kernelName]; + // create python kernel + var magicCommand = $"#!connect jupyter --kernel-name {kernelName} --kernel-spec {venv}"; + var connectCommand = new SubmitCode(magicCommand); + var result = this.compositeKernel.SendAsync(connectCommand).Result; + + result.ThrowOnCommandFailed(); + + return this; + } + + public CompositeKernel Build() + { + return this.compositeKernel + .UseDefaultMagicCommands() + .UseImportMagicCommand(); + } + + private DotnetInteractiveKernelBuilder AddKernel(Kernel kernel, IEnumerable? aliases = null) + { + this.compositeKernel.Add(kernel, aliases); + return this; + } +} +#endif diff --git a/dotnet/src/AutoGen.DotnetInteractive/Extension/AgentExtension.cs b/dotnet/src/AutoGen.DotnetInteractive/Extension/AgentExtension.cs index 83955c53fa16..de1e2a68cc0c 100644 --- a/dotnet/src/AutoGen.DotnetInteractive/Extension/AgentExtension.cs +++ b/dotnet/src/AutoGen.DotnetInteractive/Extension/AgentExtension.cs @@ -21,6 +21,7 @@ public static class AgentExtension /// [!code-csharp[Example04_Dynamic_GroupChat_Coding_Task](~/../sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs)] /// ]]> /// + [Obsolete] public static IAgent RegisterDotnetCodeBlockExectionHook( this IAgent agent, InteractiveService interactiveService, diff --git a/dotnet/src/AutoGen.DotnetInteractive/Utils.cs b/dotnet/src/AutoGen.DotnetInteractive/Extension/KernelExtension.cs similarity index 57% rename from dotnet/src/AutoGen.DotnetInteractive/Utils.cs rename to dotnet/src/AutoGen.DotnetInteractive/Extension/KernelExtension.cs index d10208d508c6..2a7afdf8857f 100644 --- a/dotnet/src/AutoGen.DotnetInteractive/Utils.cs +++ b/dotnet/src/AutoGen.DotnetInteractive/Extension/KernelExtension.cs @@ -1,23 +1,42 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// Utils.cs +// KernelExtension.cs -using System.Collections; -using System.Collections.Immutable; using Microsoft.DotNet.Interactive; using Microsoft.DotNet.Interactive.Commands; using Microsoft.DotNet.Interactive.Connection; using Microsoft.DotNet.Interactive.Events; -public static class ObservableExtensions +namespace AutoGen.DotnetInteractive.Extension; + +public static class KernelExtension { - public static SubscribedList ToSubscribedList(this IObservable source) + public static async Task RunSubmitCodeCommandAsync( + this Kernel kernel, + string codeBlock, + string targetKernelName, + CancellationToken ct = default) { - return new SubscribedList(source); + try + { + var cmd = new SubmitCode(codeBlock, targetKernelName); + var res = await kernel.SendAndThrowOnCommandFailedAsync(cmd, ct); + var events = res.Events; + var displayValues = res.Events.Where(x => x is StandardErrorValueProduced || x is StandardOutputValueProduced || x is ReturnValueProduced || x is DisplayedValueProduced) + .SelectMany(x => (x as DisplayEvent)!.FormattedValues); + + if (displayValues is null || displayValues.Count() == 0) + { + return null; + } + + return string.Join("\n", displayValues.Select(x => x.Value)); + } + catch (Exception ex) + { + return $"Error: {ex.Message}"; + } } -} -public static class KernelExtensions -{ internal static void SetUpValueSharingIfSupported(this ProxyKernel proxyKernel) { var supportedCommands = proxyKernel.KernelInfo.SupportedKernelCommands; @@ -38,7 +57,7 @@ internal static async Task SendAndThrowOnCommandFailedAsync return result; } - private static void ThrowOnCommandFailed(this KernelCommandResult result) + internal static void ThrowOnCommandFailed(this KernelCommandResult result) { var failedEvents = result.Events.OfType(); if (!failedEvents.Any()) @@ -60,27 +79,3 @@ private static void ThrowOnCommandFailed(this KernelCommandResult result) private static Exception GetException(this CommandFailed commandFailedEvent) => new Exception(commandFailedEvent.Message); } - -public class SubscribedList : IReadOnlyList, IDisposable -{ - private ImmutableArray _list = ImmutableArray.Empty; - private readonly IDisposable _subscription; - - public SubscribedList(IObservable source) - { - _subscription = source.Subscribe(x => _list = _list.Add(x)); - } - - public IEnumerator GetEnumerator() - { - return ((IEnumerable)_list).GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public int Count => _list.Length; - - public T this[int index] => _list[index]; - - public void Dispose() => _subscription.Dispose(); -} diff --git a/dotnet/src/AutoGen.DotnetInteractive/Extension/MessageExtension.cs b/dotnet/src/AutoGen.DotnetInteractive/Extension/MessageExtension.cs new file mode 100644 index 000000000000..6a8bf66c19f3 --- /dev/null +++ b/dotnet/src/AutoGen.DotnetInteractive/Extension/MessageExtension.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MessageExtension.cs + +using System.Text.RegularExpressions; + +namespace AutoGen.DotnetInteractive.Extension; + +public static class MessageExtension +{ + /// + /// Extract a single code block from a message. If the message contains multiple code blocks, only the first one will be returned. + /// + /// + /// code block prefix, e.g. ```csharp + /// code block suffix, e.g. ``` + /// + public static string? ExtractCodeBlock( + this IMessage message, + string codeBlockPrefix, + string codeBlockSuffix) + { + foreach (var codeBlock in message.ExtractCodeBlocks(codeBlockPrefix, codeBlockSuffix)) + { + return codeBlock; + } + + return null; + } + + /// + /// Extract all code blocks from a message. + /// + /// + /// code block prefix, e.g. ```csharp + /// code block suffix, e.g. ``` + /// + public static IEnumerable ExtractCodeBlocks( + this IMessage message, + string codeBlockPrefix, + string codeBlockSuffix) + { + var content = message.GetContent() ?? string.Empty; + if (string.IsNullOrWhiteSpace(content)) + { + yield break; + } + + foreach (Match match in Regex.Matches(content, $@"{codeBlockPrefix}([\s\S]*?){codeBlockSuffix}")) + { + yield return match.Groups[1].Value.Trim(); + } + } +} diff --git a/dotnet/src/AutoGen.DotnetInteractive/InteractiveService.cs b/dotnet/src/AutoGen.DotnetInteractive/InteractiveService.cs index 3797dfcff649..3381aecf5794 100644 --- a/dotnet/src/AutoGen.DotnetInteractive/InteractiveService.cs +++ b/dotnet/src/AutoGen.DotnetInteractive/InteractiveService.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Reactive.Linq; using System.Reflection; +using AutoGen.DotnetInteractive.Extension; using Microsoft.DotNet.Interactive; using Microsoft.DotNet.Interactive.Commands; using Microsoft.DotNet.Interactive.Connection; @@ -21,14 +22,6 @@ public class InteractiveService : IDisposable //private readonly ProcessJobTracker jobTracker = new ProcessJobTracker(); private string? installingDirectory; - public event EventHandler? DisplayEvent; - - public event EventHandler? Output; - - public event EventHandler? CommandFailed; - - public event EventHandler? HoverTextProduced; - /// /// Install dotnet interactive tool to /// and create an instance of . @@ -52,6 +45,8 @@ public InteractiveService(Kernel kernel) this.kernel = kernel; } + public Kernel? Kernel => this.kernel; + public async Task StartAsync(string workingDirectory, CancellationToken ct = default) { if (this.kernel != null) @@ -63,31 +58,14 @@ public async Task StartAsync(string workingDirectory, CancellationToken ct return true; } - public async Task SubmitCommandAsync(KernelCommand cmd, CancellationToken ct) + public async Task SubmitCommandAsync(SubmitCode cmd, CancellationToken ct) { if (this.kernel == null) { throw new Exception("Kernel is not running"); } - try - { - var res = await this.kernel.SendAndThrowOnCommandFailedAsync(cmd, ct); - var events = res.Events; - var displayValues = events.Where(x => x is StandardErrorValueProduced || x is StandardOutputValueProduced || x is ReturnValueProduced) - .SelectMany(x => (x as DisplayEvent)!.FormattedValues); - - if (displayValues is null || displayValues.Count() == 0) - { - return null; - } - - return string.Join("\n", displayValues.Select(x => x.Value)); - } - catch (Exception ex) - { - return $"Error: {ex.Message}"; - } + return await this.kernel.RunSubmitCodeCommandAsync(cmd.Code, cmd.TargetKernelName, ct); } public async Task SubmitPowershellCodeAsync(string code, CancellationToken ct) @@ -109,7 +87,6 @@ public bool RestoreDotnetInteractive() throw new Exception("Installing directory is not set"); } - this.WriteLine("Restore dotnet interactive tool"); // write RestoreInteractive.config from embedded resource to this.workingDirectory var assembly = Assembly.GetAssembly(typeof(InteractiveService))!; var resourceName = "AutoGen.DotnetInteractive.RestoreInteractive.config"; @@ -202,8 +179,6 @@ await rootProxyKernel.SendAsync( //compositeKernel.DefaultKernelName = "csharp"; compositeKernel.Add(rootProxyKernel); - compositeKernel.KernelEvents.Subscribe(this.OnKernelDiagnosticEventReceived); - return compositeKernel; } catch (CommandLineInvocationException) when (restoreWhenFail) @@ -219,35 +194,11 @@ await rootProxyKernel.SendAsync( } } - private void OnKernelDiagnosticEventReceived(KernelEvent ke) - { - this.WriteLine("Receive data from kernel"); - this.WriteLine(KernelEventEnvelope.Serialize(ke)); - - switch (ke) - { - case DisplayEvent de: - this.DisplayEvent?.Invoke(this, de); - break; - case CommandFailed cf: - this.CommandFailed?.Invoke(this, cf); - break; - case HoverTextProduced cf: - this.HoverTextProduced?.Invoke(this, cf); - break; - } - } - - private void WriteLine(string data) - { - this.Output?.Invoke(this, data); - } - private void PrintProcessOutput(object sender, DataReceivedEventArgs e) { if (!string.IsNullOrEmpty(e.Data)) { - this.WriteLine(e.Data); + Console.WriteLine(e.Data); } } diff --git a/dotnet/test/AutoGen.DotnetInteractive.Tests/AutoGen.DotnetInteractive.Tests.csproj b/dotnet/test/AutoGen.DotnetInteractive.Tests/AutoGen.DotnetInteractive.Tests.csproj index 7f7001a877d1..8676762015d1 100644 --- a/dotnet/test/AutoGen.DotnetInteractive.Tests/AutoGen.DotnetInteractive.Tests.csproj +++ b/dotnet/test/AutoGen.DotnetInteractive.Tests/AutoGen.DotnetInteractive.Tests.csproj @@ -13,4 +13,9 @@ + + + + + diff --git a/dotnet/test/AutoGen.DotnetInteractive.Tests/DotnetInteractiveKernelBuilderTest.cs b/dotnet/test/AutoGen.DotnetInteractive.Tests/DotnetInteractiveKernelBuilderTest.cs new file mode 100644 index 000000000000..9565f120342c --- /dev/null +++ b/dotnet/test/AutoGen.DotnetInteractive.Tests/DotnetInteractiveKernelBuilderTest.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// DotnetInteractiveKernelBuilderTest.cs + +using AutoGen.DotnetInteractive.Extension; +using FluentAssertions; +using Xunit; + +namespace AutoGen.DotnetInteractive.Tests; + +public class DotnetInteractiveKernelBuilderTest +{ + [Fact] + public async Task ItAddCSharpKernelTestAsync() + { + var kernel = DotnetInteractiveKernelBuilder + .CreateEmptyBuilder() + .AddCSharpKernel() + .Build(); + + var csharpCode = """ + #r "nuget:Microsoft.ML, 1.5.2" + Console.WriteLine("Hello, World!"); + """; + + var result = await kernel.RunSubmitCodeCommandAsync(csharpCode, "C#"); + result.Should().Contain("Hello, World!"); + } + + [Fact] + public async Task ItAddPowershellKernelTestAsync() + { + var kernel = DotnetInteractiveKernelBuilder + .CreateEmptyBuilder() + .AddPowershellKernel() + .Build(); + + var powershellCode = @" + Write-Host 'Hello, World!' + "; + + var result = await kernel.RunSubmitCodeCommandAsync(powershellCode, "pwsh"); + result.Should().Contain("Hello, World!"); + } + + [Fact] + public async Task ItAddFSharpKernelTestAsync() + { + var kernel = DotnetInteractiveKernelBuilder + .CreateEmptyBuilder() + .AddFSharpKernel() + .Build(); + + var fsharpCode = """ + #r "nuget:Microsoft.ML, 1.5.2" + printfn "Hello, World!" + """; + + var result = await kernel.RunSubmitCodeCommandAsync(fsharpCode, "F#"); + result.Should().Contain("Hello, World!"); + } + + [Fact] + public async Task ItAddPythonKernelTestAsync() + { + var kernel = DotnetInteractiveKernelBuilder + .CreateEmptyBuilder() + .AddPythonKernel("python3") + .Build(); + + var pythonCode = """ + %pip install numpy + print('Hello, World!') + """; + + var result = await kernel.RunSubmitCodeCommandAsync(pythonCode, "python"); + result.Should().Contain("Hello, World!"); + } +} diff --git a/dotnet/test/AutoGen.DotnetInteractive.Tests/MessageExtensionTests.cs b/dotnet/test/AutoGen.DotnetInteractive.Tests/MessageExtensionTests.cs new file mode 100644 index 000000000000..a886ef4985d2 --- /dev/null +++ b/dotnet/test/AutoGen.DotnetInteractive.Tests/MessageExtensionTests.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MessageExtensionTests.cs + +using AutoGen.Core; +using AutoGen.DotnetInteractive.Extension; +using FluentAssertions; +using Xunit; + +namespace AutoGen.DotnetInteractive.Tests; + +public class MessageExtensionTests +{ + [Fact] + public void ExtractCodeBlock_WithSingleCodeBlock_ShouldReturnCodeBlock() + { + // Arrange + var message = new TextMessage(Role.Assistant, "```csharp\nConsole.WriteLine(\"Hello, World!\");\n```"); + var codeBlockPrefix = "```csharp"; + var codeBlockSuffix = "```"; + + // Act + var codeBlock = message.ExtractCodeBlock(codeBlockPrefix, codeBlockSuffix); + + codeBlock.Should().BeEquivalentTo("Console.WriteLine(\"Hello, World!\");"); + } + + [Fact] + public void ExtractCodeBlock_WithMultipleCodeBlocks_ShouldReturnFirstCodeBlock() + { + // Arrange + var message = new TextMessage(Role.Assistant, "```csharp\nConsole.WriteLine(\"Hello, World!\");\n```\n```csharp\nConsole.WriteLine(\"Hello, World!\");\n```"); + var codeBlockPrefix = "```csharp"; + var codeBlockSuffix = "```"; + + // Act + var codeBlock = message.ExtractCodeBlock(codeBlockPrefix, codeBlockSuffix); + + codeBlock.Should().BeEquivalentTo("Console.WriteLine(\"Hello, World!\");"); + } + + [Fact] + public void ExtractCodeBlock_WithNoCodeBlock_ShouldReturnNull() + { + // Arrange + var message = new TextMessage(Role.Assistant, "Hello, World!"); + var codeBlockPrefix = "```csharp"; + var codeBlockSuffix = "```"; + + // Act + var codeBlock = message.ExtractCodeBlock(codeBlockPrefix, codeBlockSuffix); + + codeBlock.Should().BeNull(); + } + + [Fact] + public void ExtractCodeBlocks_WithMultipleCodeBlocks_ShouldReturnAllCodeBlocks() + { + // Arrange + var message = new TextMessage(Role.Assistant, "```csharp\nConsole.WriteLine(\"Hello, World!\");\n```\n```csharp\nConsole.WriteLine(\"Hello, World!\");\n```"); + var codeBlockPrefix = "```csharp"; + var codeBlockSuffix = "```"; + + // Act + var codeBlocks = message.ExtractCodeBlocks(codeBlockPrefix, codeBlockSuffix); + + codeBlocks.Should().HaveCount(2); + codeBlocks.ElementAt(0).Should().BeEquivalentTo("Console.WriteLine(\"Hello, World!\");"); + codeBlocks.ElementAt(1).Should().BeEquivalentTo("Console.WriteLine(\"Hello, World!\");"); + } + + [Fact] + public void ExtractCodeBlocks_WithNoCodeBlock_ShouldReturnEmpty() + { + // Arrange + var message = new TextMessage(Role.Assistant, "Hello, World!"); + var codeBlockPrefix = "```csharp"; + var codeBlockSuffix = "```"; + + // Act + var codeBlocks = message.ExtractCodeBlocks(codeBlockPrefix, codeBlockSuffix); + + codeBlocks.Should().BeEmpty(); + } +} diff --git a/dotnet/website/articles/Run-dotnet-code.md b/dotnet/website/articles/Run-dotnet-code.md index e3d8fa78a0b3..bee7e1aa3bbb 100644 --- a/dotnet/website/articles/Run-dotnet-code.md +++ b/dotnet/website/articles/Run-dotnet-code.md @@ -16,17 +16,46 @@ For example, in data analysis scenario, agent can resolve tasks like "What is th > [!WARNING] > Running arbitrary code snippet from agent response could bring risks to your system. Using this feature with caution. -## How to run dotnet code snippet? +## Use dotnet interactive kernel to execute code snippet? The built-in feature of running dotnet code snippet is provided by [dotnet-interactive](https://github.com/dotnet/interactive). To run dotnet code snippet, you need to install the following package to your project, which provides the intergraion with dotnet-interactive: ```xml ``` -Then you can use @AutoGen.DotnetInteractive.AgentExtension.RegisterDotnetCodeBlockExectionHook(AutoGen.IAgent,InteractiveService,System.String,System.String) to register a `reply hook` to run dotnet code snippet. The hook will check if a csharp code snippet is present in the most recent message from history, and run the code snippet if it is present. - -The following code snippet shows how to register a dotnet code snippet execution hook: - -[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs?name=code_snippet_0_1)] +Then you can use @AutoGen.DotnetInteractive.DotnetInteractiveKernelBuilder* to create a in-process dotnet-interactive composite kernel with C# and F# kernels. [!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs?name=code_snippet_1_1)] + +After that, use @AutoGen.DotnetInteractive.Extension.RunSubmitCodeCommandAsync* method to run code snippet. The method will return the result of the code snippet. [!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs?name=code_snippet_1_2)] + +## Run python code snippet +To run python code, firstly you need to have python installed on your machine, then you need to set up ipykernel and jupyter in your environment. + +```bash +pip install ipykernel +pip install jupyter +``` + +After `ipykernel` and `jupyter` are installed, you can confirm the ipykernel is installed correctly by running the following command: + +```bash +jupyter kernelspec list +``` + +The output should contain all available kernels, including `python3`. + +```bash +Available kernels: + python3 /usr/local/share/jupyter/kernels/python3 + ... +``` + +Then you can add the python kernel to the dotnet-interactive composite kernel by calling `AddPythonKernel` method. + +[!code-csharp[](../../sample/AutoGen.BasicSamples/CodeSnippet/RunCodeSnippetCodeSnippet.cs?name=code_snippet_1_4)] + +## Further reading +You can refer to the following examples for running code snippet in agentic workflow: +- Dynamic_GroupChat_Coding_Task: [![](https://img.shields.io/badge/Open%20on%20Github-grey?logo=github)](https://github.com/microsoft/autogen/blob/main/dotnet/sample/AutoGen.BasicSample/Example04_Dynamic_GroupChat_Coding_Task.cs) +- Dynamic_GroupChat_Calculate_Fibonacci: [![](https://img.shields.io/badge/Open%20on%20Github-grey?logo=github)](https://github.com/microsoft/autogen/blob/main/dotnet/sample/AutoGen.BasicSample/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs)