From 374270f9c70582c6f48e053aaa9d7ed0613ddf81 Mon Sep 17 00:00:00 2001 From: Xiaoyun Zhang Date: Thu, 9 May 2024 15:36:20 -0700 Subject: [PATCH] [.Net] Add KernelPluginMiddleware in AutoGen.SemanticKernel (#2595) * add kernel function middleware * update * fix format * fix build error --- dotnet/AutoGen.sln | 11 +- dotnet/eng/MetaInfo.props | 2 +- .../CodeSnippet/MiddlewareAgentCodeSnippet.cs | 2 +- .../CodeSnippet/MistralAICodeSnippet.cs | 4 +- .../Example01_AssistantAgent.cs | 2 +- .../Example02_TwoAgent_MathChat.cs | 2 +- .../Example03_Agent_FunctionCall.cs | 2 +- ...Example04_Dynamic_GroupChat_Coding_Task.cs | 2 +- ...7_Dynamic_GroupChat_Calculate_Fibonacci.cs | 2 +- ...Example14_MistralClientAgent_TokenCount.cs | 2 +- .../Example15_GPT4V_BinaryDataImageMessage.cs | 2 +- .../DTOs/ChatCompletionResponse.cs | 2 +- dotnet/src/AutoGen.Mistral/DTOs/Error.cs | 2 +- dotnet/src/AutoGen.Mistral/DTOs/Model.cs | 2 +- .../AutoGen.SemanticKernel.csproj | 3 - .../Extension/KernelExtension.cs | 34 +++++ .../Middleware/KernelPluginMiddleware.cs | 77 +++++++++++ .../MistralClientAgentTests.cs | 2 +- ...teFunctionContractsFromMethod.approved.txt | 24 ++++ ...teFunctionContractsFromPrompt.approved.txt | 8 ++ ...nctionContractsFromTestPlugin.approved.txt | 26 ++++ .../AutoGen.SemanticKernel.Tests.csproj | 27 ++++ .../KernelFunctionExtensionTests.cs | 104 +++++++++++++++ .../KernelFunctionMiddlewareTests.cs | 121 ++++++++++++++++++ .../SemanticKernelAgentTest.cs | 8 +- dotnet/test/AutoGen.Tests/GlobalUsing.cs | 2 +- dotnet/website/update.md | 6 + 27 files changed, 455 insertions(+), 26 deletions(-) create mode 100644 dotnet/src/AutoGen.SemanticKernel/Middleware/KernelPluginMiddleware.cs create mode 100644 dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromMethod.approved.txt create mode 100644 dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromPrompt.approved.txt create mode 100644 dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromTestPlugin.approved.txt create mode 100644 dotnet/test/AutoGen.SemanticKernel.Tests/AutoGen.SemanticKernel.Tests.csproj create mode 100644 dotnet/test/AutoGen.SemanticKernel.Tests/KernelFunctionExtensionTests.cs create mode 100644 dotnet/test/AutoGen.SemanticKernel.Tests/KernelFunctionMiddlewareTests.cs rename dotnet/test/{AutoGen.Tests => AutoGen.SemanticKernel.Tests}/SemanticKernelAgentTest.cs (98%) diff --git a/dotnet/AutoGen.sln b/dotnet/AutoGen.sln index 3841b9acf7..b46b8091cf 100644 --- a/dotnet/AutoGen.sln +++ b/dotnet/AutoGen.sln @@ -29,9 +29,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Core", "src\AutoGen EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.OpenAI", "src\AutoGen.OpenAI\AutoGen.OpenAI.csproj", "{63445BB7-DBB9-4AEF-9D6F-98BBE75EE1EC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.Mistral", "src\AutoGen.Mistral\AutoGen.Mistral.csproj", "{6585D1A4-3D97-4D76-A688-1933B61AEB19}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Mistral", "src\AutoGen.Mistral\AutoGen.Mistral.csproj", "{6585D1A4-3D97-4D76-A688-1933B61AEB19}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.Mistral.Tests", "test\AutoGen.Mistral.Tests\AutoGen.Mistral.Tests.csproj", "{15441693-3659-4868-B6C1-B106F52FF3BA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Mistral.Tests", "test\AutoGen.Mistral.Tests\AutoGen.Mistral.Tests.csproj", "{15441693-3659-4868-B6C1-B106F52FF3BA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.SemanticKernel.Tests", "test\AutoGen.SemanticKernel.Tests\AutoGen.SemanticKernel.Tests.csproj", "{1DFABC4A-8458-4875-8DCB-59F3802DAC65}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -87,6 +89,10 @@ Global {15441693-3659-4868-B6C1-B106F52FF3BA}.Debug|Any CPU.Build.0 = Debug|Any CPU {15441693-3659-4868-B6C1-B106F52FF3BA}.Release|Any CPU.ActiveCfg = Release|Any CPU {15441693-3659-4868-B6C1-B106F52FF3BA}.Release|Any CPU.Build.0 = Release|Any CPU + {1DFABC4A-8458-4875-8DCB-59F3802DAC65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1DFABC4A-8458-4875-8DCB-59F3802DAC65}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DFABC4A-8458-4875-8DCB-59F3802DAC65}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1DFABC4A-8458-4875-8DCB-59F3802DAC65}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -104,6 +110,7 @@ Global {63445BB7-DBB9-4AEF-9D6F-98BBE75EE1EC} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} {6585D1A4-3D97-4D76-A688-1933B61AEB19} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} {15441693-3659-4868-B6C1-B106F52FF3BA} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} + {1DFABC4A-8458-4875-8DCB-59F3802DAC65} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {93384647-528D-46C8-922C-8DB36A382F0B} diff --git a/dotnet/eng/MetaInfo.props b/dotnet/eng/MetaInfo.props index 4c354d8fee..8aff3c6022 100644 --- a/dotnet/eng/MetaInfo.props +++ b/dotnet/eng/MetaInfo.props @@ -1,7 +1,7 @@ - 0.0.12 + 0.0.13 AutoGen https://microsoft.github.io/autogen-for-net/ https://github.com/microsoft/autogen diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs index 8be026552e..320afd0de6 100644 --- a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MiddlewareAgentCodeSnippet.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // MiddlewareAgentCodeSnippet.cs -using AutoGen.Core; using System.Text.Json; +using AutoGen.Core; using AutoGen.OpenAI; using FluentAssertions; diff --git a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs index cd49810dc6..0ce1d840d3 100644 --- a/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs +++ b/dotnet/sample/AutoGen.BasicSamples/CodeSnippet/MistralAICodeSnippet.cs @@ -2,8 +2,8 @@ // MistralAICodeSnippet.cs #region using_statement -using AutoGen.Mistral; using AutoGen.Core; +using AutoGen.Mistral; using AutoGen.Mistral.Extension; using FluentAssertions; #endregion using_statement @@ -83,4 +83,4 @@ internal class MistralAICodeSnippet reply.GetContent().Should().Be("The weather in Seattle is sunny."); #endregion send_message_with_function_call } -} \ No newline at end of file +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example01_AssistantAgent.cs b/dotnet/sample/AutoGen.BasicSamples/Example01_AssistantAgent.cs index 8797bda831..3ee363bfc0 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example01_AssistantAgent.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example01_AssistantAgent.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Example01_AssistantAgent.cs -using AutoGen.Core; using AutoGen; using AutoGen.BasicSample; +using AutoGen.Core; using FluentAssertions; /// diff --git a/dotnet/sample/AutoGen.BasicSamples/Example02_TwoAgent_MathChat.cs b/dotnet/sample/AutoGen.BasicSamples/Example02_TwoAgent_MathChat.cs index f20b0848a3..c2957f32da 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example02_TwoAgent_MathChat.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example02_TwoAgent_MathChat.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Example02_TwoAgent_MathChat.cs -using AutoGen.Core; using AutoGen; using AutoGen.BasicSample; +using AutoGen.Core; using FluentAssertions; public static class Example02_TwoAgent_MathChat { diff --git a/dotnet/sample/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs b/dotnet/sample/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs index bfb8d71095..57b9ea76dc 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs @@ -2,8 +2,8 @@ // Example03_Agent_FunctionCall.cs using AutoGen; -using AutoGen.Core; using AutoGen.BasicSample; +using AutoGen.Core; using FluentAssertions; /// 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 d9489e522e..c5d9a01f97 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example04_Dynamic_GroupChat_Coding_Task.cs @@ -2,8 +2,8 @@ // Example04_Dynamic_GroupChat_Coding_Task.cs using AutoGen; -using AutoGen.Core; using AutoGen.BasicSample; +using AutoGen.Core; using AutoGen.DotnetInteractive; using AutoGen.OpenAI; using FluentAssertions; 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 89e6f45f89..6584baa5fa 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs @@ -5,8 +5,8 @@ using System.Text; using System.Text.Json; using AutoGen; using AutoGen.BasicSample; -using AutoGen.DotnetInteractive; using AutoGen.Core; +using AutoGen.DotnetInteractive; using AutoGen.OpenAI; using FluentAssertions; diff --git a/dotnet/sample/AutoGen.BasicSamples/Example14_MistralClientAgent_TokenCount.cs b/dotnet/sample/AutoGen.BasicSamples/Example14_MistralClientAgent_TokenCount.cs index 8b20dbf33a..4c8794de96 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example14_MistralClientAgent_TokenCount.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example14_MistralClientAgent_TokenCount.cs @@ -62,4 +62,4 @@ public class Example14_MistralClientAgent_TokenCount tokenCounterMiddleware.GetCompletionTokenCount().Should().BeGreaterThan(0); #endregion chat_with_agent } -} \ No newline at end of file +} diff --git a/dotnet/sample/AutoGen.BasicSamples/Example15_GPT4V_BinaryDataImageMessage.cs b/dotnet/sample/AutoGen.BasicSamples/Example15_GPT4V_BinaryDataImageMessage.cs index 7a3422cb86..f376342ed8 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example15_GPT4V_BinaryDataImageMessage.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example15_GPT4V_BinaryDataImageMessage.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// Example15_ImageMessage.cs +// Example15_GPT4V_BinaryDataImageMessage.cs using AutoGen.Core; using AutoGen.OpenAI; diff --git a/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs b/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs index 13e29e7139..ff241f8d34 100644 --- a/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs +++ b/dotnet/src/AutoGen.Mistral/DTOs/ChatCompletionResponse.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // ChatCompletionResponse.cs using System.Collections.Generic; diff --git a/dotnet/src/AutoGen.Mistral/DTOs/Error.cs b/dotnet/src/AutoGen.Mistral/DTOs/Error.cs index 8bddcfc776..77eb2d341f 100644 --- a/dotnet/src/AutoGen.Mistral/DTOs/Error.cs +++ b/dotnet/src/AutoGen.Mistral/DTOs/Error.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Error.cs using System.Text.Json.Serialization; diff --git a/dotnet/src/AutoGen.Mistral/DTOs/Model.cs b/dotnet/src/AutoGen.Mistral/DTOs/Model.cs index 70a4b3c997..915d2f737e 100644 --- a/dotnet/src/AutoGen.Mistral/DTOs/Model.cs +++ b/dotnet/src/AutoGen.Mistral/DTOs/Model.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Model.cs using System; diff --git a/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj b/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj index be2fa0a574..3bd96f93b6 100644 --- a/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj +++ b/dotnet/src/AutoGen.SemanticKernel/AutoGen.SemanticKernel.csproj @@ -3,9 +3,6 @@ netstandard2.0 AutoGen.SemanticKernel - - - $(NoWarn);SKEXP0110 diff --git a/dotnet/src/AutoGen.SemanticKernel/Extension/KernelExtension.cs b/dotnet/src/AutoGen.SemanticKernel/Extension/KernelExtension.cs index f1589ab09e..8eb11934da 100644 --- a/dotnet/src/AutoGen.SemanticKernel/Extension/KernelExtension.cs +++ b/dotnet/src/AutoGen.SemanticKernel/Extension/KernelExtension.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // KernelExtension.cs +using System.Linq; using Microsoft.SemanticKernel; namespace AutoGen.SemanticKernel.Extension; @@ -11,4 +12,37 @@ public static class KernelExtension { return new SemanticKernelAgent(kernel, name, systemMessage, settings); } + + /// + /// Convert a to a + /// + /// kernel function metadata + public static FunctionContract ToFunctionContract(this KernelFunctionMetadata metadata) + { + return new FunctionContract() + { + Name = metadata.Name, + Description = metadata.Description, + Parameters = metadata.Parameters.Select(p => p.ToFunctionParameterContract()).ToList(), + ReturnType = metadata.ReturnParameter.ParameterType, + ReturnDescription = metadata.ReturnParameter.Description, + ClassName = metadata.PluginName, + }; + } + + /// + /// Convert a to a + /// + /// kernel parameter metadata + public static FunctionParameterContract ToFunctionParameterContract(this KernelParameterMetadata metadata) + { + return new FunctionParameterContract() + { + Name = metadata.Name, + Description = metadata.Description, + DefaultValue = metadata.DefaultValue, + IsRequired = metadata.IsRequired, + ParameterType = metadata.ParameterType, + }; + } } diff --git a/dotnet/src/AutoGen.SemanticKernel/Middleware/KernelPluginMiddleware.cs b/dotnet/src/AutoGen.SemanticKernel/Middleware/KernelPluginMiddleware.cs new file mode 100644 index 0000000000..628915a030 --- /dev/null +++ b/dotnet/src/AutoGen.SemanticKernel/Middleware/KernelPluginMiddleware.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// KernelPluginMiddleware.cs + +using System; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.SemanticKernel.Extension; +using Microsoft.SemanticKernel; + +namespace AutoGen.SemanticKernel; + +/// +/// A middleware that consumes +/// +public class KernelPluginMiddleware : IMiddleware +{ + private readonly KernelPlugin _kernelPlugin; + private readonly FunctionCallMiddleware _functionCallMiddleware; + public string? Name => nameof(KernelPluginMiddleware); + + public KernelPluginMiddleware(Kernel kernel, KernelPlugin kernelPlugin) + { + _kernelPlugin = kernelPlugin; + var functionContracts = kernelPlugin.Select(k => k.Metadata.ToFunctionContract()); + var functionMap = kernelPlugin.ToDictionary(kv => kv.Metadata.Name, kv => InvokeFunctionPartial(kernel, kv)); + _functionCallMiddleware = new FunctionCallMiddleware(functionContracts, functionMap, Name); + } + + public Task InvokeAsync(MiddlewareContext context, IAgent agent, CancellationToken cancellationToken = default) + { + return _functionCallMiddleware.InvokeAsync(context, agent, cancellationToken); + } + + private async Task InvokeFunctionAsync(Kernel kernel, KernelFunction function, string arguments) + { + var kernelArguments = new KernelArguments(); + var parameters = function.Metadata.Parameters; + var jsonObject = JsonSerializer.Deserialize(arguments) ?? new JsonObject(); + foreach (var parameter in parameters) + { + var parameterName = parameter.Name; + if (jsonObject.ContainsKey(parameterName)) + { + var parameterType = parameter.ParameterType ?? throw new ArgumentException($"Missing parameter type for {parameterName}"); + var parameterValue = jsonObject[parameterName]; + var parameterObject = parameterValue.Deserialize(parameterType); + kernelArguments.Add(parameterName, parameterObject); + } + else + { + if (parameter.DefaultValue != null) + { + kernelArguments.Add(parameterName, parameter.DefaultValue); + } + else if (parameter.IsRequired) + { + throw new ArgumentException($"Missing required parameter: {parameterName}"); + } + } + } + var result = await function.InvokeAsync(kernel, kernelArguments); + + return result.ToString(); + } + + private Func> InvokeFunctionPartial(Kernel kernel, KernelFunction function) + { + return async (string args) => + { + var result = await InvokeFunctionAsync(kernel, function, args); + return result.ToString(); + }; + } +} diff --git a/dotnet/test/AutoGen.Mistral.Tests/MistralClientAgentTests.cs b/dotnet/test/AutoGen.Mistral.Tests/MistralClientAgentTests.cs index bcd5f1309f..2b6839dd0e 100644 --- a/dotnet/test/AutoGen.Mistral.Tests/MistralClientAgentTests.cs +++ b/dotnet/test/AutoGen.Mistral.Tests/MistralClientAgentTests.cs @@ -92,7 +92,7 @@ public partial class MistralClientAgentTests new TextMessage(Role.User, "what's the weather in Seattle?"), new ToolCallMessage(this.GetWeatherFunctionContract.Name!, weatherFunctionArgumets, from: agent.Name), new ToolCallResultMessage(functionCallResult, this.GetWeatherFunctionContract.Name!, weatherFunctionArgumets), - ]; + ]; var reply = await agent.SendAsync(chatHistory: chatHistory); diff --git a/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromMethod.approved.txt b/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromMethod.approved.txt new file mode 100644 index 0000000000..677831d412 --- /dev/null +++ b/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromMethod.approved.txt @@ -0,0 +1,24 @@ +[ + { + "Name": "_ItCreateFunctionContractsFromMethod_b__2_0", + "Description": "", + "Parameters": [], + "ReturnType": "System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e", + "ReturnDescription": "" + }, + { + "Name": "_ItCreateFunctionContractsFromMethod_b__2_1", + "Description": "", + "Parameters": [ + { + "Name": "message", + "Description": "", + "ParameterType": "System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e", + "IsRequired": true, + "DefaultValue": "" + } + ], + "ReturnType": "System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e", + "ReturnDescription": "" + } +] \ No newline at end of file diff --git a/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromPrompt.approved.txt b/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromPrompt.approved.txt new file mode 100644 index 0000000000..428f53572f --- /dev/null +++ b/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromPrompt.approved.txt @@ -0,0 +1,8 @@ +[ + { + "Name": "sayHello", + "Description": "Generic function, unknown purpose", + "Parameters": [], + "ReturnDescription": "" + } +] \ No newline at end of file diff --git a/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromTestPlugin.approved.txt b/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromTestPlugin.approved.txt new file mode 100644 index 0000000000..ee835b1ba0 --- /dev/null +++ b/dotnet/test/AutoGen.SemanticKernel.Tests/ApprovalTests/KernelFunctionExtensionTests.ItCreateFunctionContractsFromTestPlugin.approved.txt @@ -0,0 +1,26 @@ +[ + { + "ClassName": "test_plugin", + "Name": "GetState", + "Description": "Gets the state of the light.", + "Parameters": [], + "ReturnType": "System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e", + "ReturnDescription": "" + }, + { + "ClassName": "test_plugin", + "Name": "ChangeState", + "Description": "Changes the state of the light.'", + "Parameters": [ + { + "Name": "newState", + "Description": "new state", + "ParameterType": "System.Boolean, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e", + "IsRequired": true, + "DefaultValue": "" + } + ], + "ReturnType": "System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e", + "ReturnDescription": "" + } +] \ No newline at end of file diff --git a/dotnet/test/AutoGen.SemanticKernel.Tests/AutoGen.SemanticKernel.Tests.csproj b/dotnet/test/AutoGen.SemanticKernel.Tests/AutoGen.SemanticKernel.Tests.csproj new file mode 100644 index 0000000000..b6d03ddc4a --- /dev/null +++ b/dotnet/test/AutoGen.SemanticKernel.Tests/AutoGen.SemanticKernel.Tests.csproj @@ -0,0 +1,27 @@ + + + + $(TestTargetFramework) + enable + false + $(NoWarn);SKEXP0110 + True + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/test/AutoGen.SemanticKernel.Tests/KernelFunctionExtensionTests.cs b/dotnet/test/AutoGen.SemanticKernel.Tests/KernelFunctionExtensionTests.cs new file mode 100644 index 0000000000..c898c98b3c --- /dev/null +++ b/dotnet/test/AutoGen.SemanticKernel.Tests/KernelFunctionExtensionTests.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// KernelFunctionExtensionTests.cs + +using System.ComponentModel; +using ApprovalTests; +using ApprovalTests.Namers; +using ApprovalTests.Reporters; +using AutoGen.SemanticKernel.Extension; +using FluentAssertions; +using Microsoft.SemanticKernel; +using Newtonsoft.Json; +using Xunit; + +namespace AutoGen.SemanticKernel.Tests; + +public class TestPlugin +{ + public bool IsOn { get; set; } = false; + + [KernelFunction] + [Description("Gets the state of the light.")] + public string GetState() => this.IsOn ? "on" : "off"; + + [KernelFunction] + [Description("Changes the state of the light.'")] + public string ChangeState( + [Description("new state")] bool newState) + { + this.IsOn = newState; + var state = this.GetState(); + + // Print the state to the console + Console.ForegroundColor = ConsoleColor.DarkBlue; + Console.WriteLine($"[Light is now {state}]"); + Console.ResetColor(); + + return $"The status of the light is now {state}"; + } +} +public class KernelFunctionExtensionTests +{ + private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + StringEscapeHandling = StringEscapeHandling.Default, + }; + + [Fact] + [UseReporter(typeof(DiffReporter))] + [UseApprovalSubdirectory("ApprovalTests")] + public void ItCreateFunctionContractsFromTestPlugin() + { + var kernel = new Kernel(); + var plugin = kernel.ImportPluginFromType("test_plugin"); + + var functionContracts = plugin.Select(f => f.Metadata.ToFunctionContract()).ToList(); + + functionContracts.Count.Should().Be(2); + var json = JsonConvert.SerializeObject(functionContracts, _serializerSettings); + + Approvals.Verify(json); + } + + [Fact] + [UseReporter(typeof(DiffReporter))] + [UseApprovalSubdirectory("ApprovalTests")] + public void ItCreateFunctionContractsFromMethod() + { + var kernel = new Kernel(); + var sayHelloFunction = KernelFunctionFactory.CreateFromMethod(() => "Hello, World!"); + var echoFunction = KernelFunctionFactory.CreateFromMethod((string message) => message); + + var functionContracts = new[] + { + sayHelloFunction.Metadata.ToFunctionContract(), + echoFunction.Metadata.ToFunctionContract(), + }; + + var json = JsonConvert.SerializeObject(functionContracts, _serializerSettings); + + functionContracts.Length.Should().Be(2); + Approvals.Verify(json); + } + + [Fact] + [UseReporter(typeof(DiffReporter))] + [UseApprovalSubdirectory("ApprovalTests")] + public void ItCreateFunctionContractsFromPrompt() + { + var kernel = new Kernel(); + var sayHelloFunction = KernelFunctionFactory.CreateFromPrompt("Say {{hello}}, World!", functionName: "sayHello"); + + var functionContracts = new[] + { + sayHelloFunction.Metadata.ToFunctionContract(), + }; + + var json = JsonConvert.SerializeObject(functionContracts, _serializerSettings); + + functionContracts.Length.Should().Be(1); + Approvals.Verify(json); + } +} diff --git a/dotnet/test/AutoGen.SemanticKernel.Tests/KernelFunctionMiddlewareTests.cs b/dotnet/test/AutoGen.SemanticKernel.Tests/KernelFunctionMiddlewareTests.cs new file mode 100644 index 0000000000..f560419e8c --- /dev/null +++ b/dotnet/test/AutoGen.SemanticKernel.Tests/KernelFunctionMiddlewareTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// KernelFunctionMiddlewareTests.cs + +using AutoGen.Core; +using AutoGen.OpenAI; +using AutoGen.OpenAI.Extension; +using AutoGen.Tests; +using Azure.AI.OpenAI; +using FluentAssertions; +using Microsoft.SemanticKernel; + +namespace AutoGen.SemanticKernel.Tests; + +public class KernelFunctionMiddlewareTests +{ + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task ItRegisterKernelFunctionMiddlewareFromTestPluginTests() + { + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var openaiClient = new OpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(key)); + + var kernel = new Kernel(); + var plugin = kernel.ImportPluginFromType(); + var kernelFunctionMiddleware = new KernelPluginMiddleware(kernel, plugin); + + var agent = new OpenAIChatAgent(openaiClient, "assistant", modelName: "gpt-35-turbo-16k") + .RegisterMessageConnector() + .RegisterMiddleware(kernelFunctionMiddleware); + + var reply = await agent.SendAsync("what's the status of the light?"); + reply.GetContent().Should().Be("off"); + reply.Should().BeOfType>(); + if (reply is AggregateMessage aggregateMessage) + { + var toolCallMessage = aggregateMessage.Message1; + toolCallMessage.ToolCalls.Should().HaveCount(1); + toolCallMessage.ToolCalls[0].FunctionName.Should().Be("GetState"); + + var toolCallResultMessage = aggregateMessage.Message2; + toolCallResultMessage.ToolCalls.Should().HaveCount(1); + toolCallResultMessage.ToolCalls[0].Result.Should().Be("off"); + } + + reply = await agent.SendAsync("change the status of the light to on"); + reply.GetContent().Should().Be("The status of the light is now on"); + reply.Should().BeOfType>(); + if (reply is AggregateMessage aggregateMessage1) + { + var toolCallMessage = aggregateMessage1.Message1; + toolCallMessage.ToolCalls.Should().HaveCount(1); + toolCallMessage.ToolCalls[0].FunctionName.Should().Be("ChangeState"); + + var toolCallResultMessage = aggregateMessage1.Message2; + toolCallResultMessage.ToolCalls.Should().HaveCount(1); + } + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")] + public async Task ItRegisterKernelFunctionMiddlewareFromMethodTests() + { + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var openaiClient = new OpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(key)); + + var kernel = new Kernel(); + var getWeatherMethod = kernel.CreateFunctionFromMethod((string location) => $"The weather in {location} is sunny.", functionName: "GetWeather", description: "Get the weather for a location."); + var createPersonObjectMethod = kernel.CreateFunctionFromMethod((string name, string email, int age) => new Person(name, email, age), functionName: "CreatePersonObject", description: "Creates a person object."); + var plugin = kernel.ImportPluginFromFunctions("plugin", [getWeatherMethod, createPersonObjectMethod]); + var kernelFunctionMiddleware = new KernelPluginMiddleware(kernel, plugin); + + var agent = new OpenAIChatAgent(openaiClient, "assistant", modelName: "gpt-35-turbo-16k") + .RegisterMessageConnector() + .RegisterMiddleware(kernelFunctionMiddleware); + + var reply = await agent.SendAsync("what's the weather in Seattle?"); + reply.GetContent().Should().Be("The weather in Seattle is sunny."); + reply.Should().BeOfType>(); + if (reply is AggregateMessage getWeatherMessage) + { + var toolCallMessage = getWeatherMessage.Message1; + toolCallMessage.ToolCalls.Should().HaveCount(1); + toolCallMessage.ToolCalls[0].FunctionName.Should().Be("GetWeather"); + + var toolCallResultMessage = getWeatherMessage.Message2; + toolCallResultMessage.ToolCalls.Should().HaveCount(1); + } + + reply = await agent.SendAsync("Create a person object with name: John, email: 12345@gmail.com, age: 30"); + reply.GetContent().Should().Be("Name: John, Email: 12345@gmail.com, Age: 30"); + reply.Should().BeOfType>(); + if (reply is AggregateMessage createPersonObjectMessage) + { + var toolCallMessage = createPersonObjectMessage.Message1; + toolCallMessage.ToolCalls.Should().HaveCount(1); + toolCallMessage.ToolCalls[0].FunctionName.Should().Be("CreatePersonObject"); + + var toolCallResultMessage = createPersonObjectMessage.Message2; + toolCallResultMessage.ToolCalls.Should().HaveCount(1); + } + } +} + +public class Person +{ + public Person(string name, string email, int age) + { + this.Name = name; + this.Email = email; + this.Age = age; + } + + public string Name { get; set; } + public string Email { get; set; } + public int Age { get; set; } + + public override string ToString() + { + return $"Name: {this.Name}, Email: {this.Email}, Age: {this.Age}"; + } +} diff --git a/dotnet/test/AutoGen.Tests/SemanticKernelAgentTest.cs b/dotnet/test/AutoGen.SemanticKernel.Tests/SemanticKernelAgentTest.cs similarity index 98% rename from dotnet/test/AutoGen.Tests/SemanticKernelAgentTest.cs rename to dotnet/test/AutoGen.SemanticKernel.Tests/SemanticKernelAgentTest.cs index 0fcf5a6abe..14c27cb48a 100644 --- a/dotnet/test/AutoGen.Tests/SemanticKernelAgentTest.cs +++ b/dotnet/test/AutoGen.SemanticKernel.Tests/SemanticKernelAgentTest.cs @@ -1,18 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // SemanticKernelAgentTest.cs -using System; -using System.Linq; -using System.Threading.Tasks; -using AutoGen.SemanticKernel; +using AutoGen.Core; using AutoGen.SemanticKernel.Extension; +using AutoGen.Tests; using FluentAssertions; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; -namespace AutoGen.Tests; +namespace AutoGen.SemanticKernel.Tests; public partial class SemanticKernelAgentTest { diff --git a/dotnet/test/AutoGen.Tests/GlobalUsing.cs b/dotnet/test/AutoGen.Tests/GlobalUsing.cs index d00ae3ce4f..d66bf001ed 100644 --- a/dotnet/test/AutoGen.Tests/GlobalUsing.cs +++ b/dotnet/test/AutoGen.Tests/GlobalUsing.cs @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// globalUsing.cs +// GlobalUsing.cs global using AutoGen.Core; diff --git a/dotnet/website/update.md b/dotnet/website/update.md index 3d905c0ab1..cd2b25fa5c 100644 --- a/dotnet/website/update.md +++ b/dotnet/website/update.md @@ -1,7 +1,13 @@ ##### Update +###### New features +- [Issue 2593](https://github.com/microsoft/autogen/issues/2593) In AutoGen.SemanticKernel, add `KernelPluginMiddleware` to support consume semantic kernel plugin in `IAgent`. + +###### API Breaking Changes - [API Breaking Change] Update the return type of `IStreamingAgent.GenerateStreamingReplyAsync` from `Task>` to `IAsyncEnumerable` - [API Breaking Change] Update the return type of `IStreamingMiddleware.InvokeAsync` from `Task>` to `IAsyncEnumerable` - [API Breaking Change] Mark `RegisterReply`, `RegisterPreProcess` and `RegisterPostProcess` as obsolete. You can replace them with `RegisterMiddleware` + +###### Bug Fixes - Fix [Issue 2609](https://github.com/microsoft/autogen/issues/2609) ##### Update on 0.0.12 (2024-04-22) - Add AutoGen.Mistral package to support Mistral.AI models