From 4d1ecb11d85cf95f9abfa24872086d7890e39bc2 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 21 Mar 2025 23:02:44 -0400 Subject: [PATCH 1/4] Update MCP sample with ModelContextProtocol --- dotnet/Directory.Packages.props | 2 +- dotnet/SK-dotnet.sln | 18 +- .../McpDotNetExtensions.cs | 159 ------------------ .../ModelContextProtocolPlugin.csproj} | 10 +- .../Program.cs | 50 +++--- .../README.md | 6 +- .../AIFunctionKernelFunction.cs | 21 +-- .../ChatCompletionServiceChatClient.cs | 2 +- .../Functions/KernelFunction.cs | 11 ++ 9 files changed, 63 insertions(+), 216 deletions(-) delete mode 100644 dotnet/samples/Demos/ModelContextProtocol/McpDotNetExtensions.cs rename dotnet/samples/Demos/{ModelContextProtocol/ModelContextProtocol.csproj => ModelContextProtocolPlugin/ModelContextProtocolPlugin.csproj} (81%) rename dotnet/samples/Demos/{ModelContextProtocol => ModelContextProtocolPlugin}/Program.cs (52%) rename dotnet/samples/Demos/{ModelContextProtocol => ModelContextProtocolPlugin}/README.md (78%) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 290c5501bc07..ac42f011bbf7 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -25,7 +25,7 @@ - + diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 0a4bf8c39da2..03d633bcb24b 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -483,12 +483,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessFramework.Aspire.Tra EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agents.Bedrock", "src\Agents\Bedrock\Agents.Bedrock.csproj", "{8C658E1E-83C8-4127-B8BF-27A638A45DDD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol", "samples\Demos\ModelContextProtocol\ModelContextProtocol.csproj", "{B16AC373-3DA8-4505-9510-110347CD635D}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlServerIntegrationTests", "src\VectorDataIntegrationTests\SqlServerIntegrationTests\SqlServerIntegrationTests.csproj", "{A5E6193C-8431-4C6E-B674-682CB41EAA0C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PineconeIntegrationTests", "src\VectorDataIntegrationTests\PineconeIntegrationTests\PineconeIntegrationTests.csproj", "{E9A74E0C-BC02-4DDD-A487-89847EDF8026}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocolPlugin", "samples\Demos\ModelContextProtocolPlugin\ModelContextProtocolPlugin.csproj", "{801C9CE4-53AF-D2DB-E0D6-9A6BB47E9654}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1324,12 +1324,6 @@ Global {8C658E1E-83C8-4127-B8BF-27A638A45DDD}.Publish|Any CPU.Build.0 = Publish|Any CPU {8C658E1E-83C8-4127-B8BF-27A638A45DDD}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C658E1E-83C8-4127-B8BF-27A638A45DDD}.Release|Any CPU.Build.0 = Release|Any CPU - {B16AC373-3DA8-4505-9510-110347CD635D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B16AC373-3DA8-4505-9510-110347CD635D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B16AC373-3DA8-4505-9510-110347CD635D}.Publish|Any CPU.ActiveCfg = Debug|Any CPU - {B16AC373-3DA8-4505-9510-110347CD635D}.Publish|Any CPU.Build.0 = Debug|Any CPU - {B16AC373-3DA8-4505-9510-110347CD635D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B16AC373-3DA8-4505-9510-110347CD635D}.Release|Any CPU.Build.0 = Release|Any CPU {A5E6193C-8431-4C6E-B674-682CB41EAA0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A5E6193C-8431-4C6E-B674-682CB41EAA0C}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5E6193C-8431-4C6E-B674-682CB41EAA0C}.Publish|Any CPU.ActiveCfg = Debug|Any CPU @@ -1342,6 +1336,12 @@ Global {E9A74E0C-BC02-4DDD-A487-89847EDF8026}.Publish|Any CPU.Build.0 = Release|Any CPU {E9A74E0C-BC02-4DDD-A487-89847EDF8026}.Release|Any CPU.ActiveCfg = Release|Any CPU {E9A74E0C-BC02-4DDD-A487-89847EDF8026}.Release|Any CPU.Build.0 = Release|Any CPU + {801C9CE4-53AF-D2DB-E0D6-9A6BB47E9654}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {801C9CE4-53AF-D2DB-E0D6-9A6BB47E9654}.Debug|Any CPU.Build.0 = Debug|Any CPU + {801C9CE4-53AF-D2DB-E0D6-9A6BB47E9654}.Publish|Any CPU.ActiveCfg = Release|Any CPU + {801C9CE4-53AF-D2DB-E0D6-9A6BB47E9654}.Publish|Any CPU.Build.0 = Release|Any CPU + {801C9CE4-53AF-D2DB-E0D6-9A6BB47E9654}.Release|Any CPU.ActiveCfg = Release|Any CPU + {801C9CE4-53AF-D2DB-E0D6-9A6BB47E9654}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1522,9 +1522,9 @@ Global {37381352-4F10-427F-AB8A-51FEAB265201} = {3F260A77-B6C9-97FD-1304-4B34DA936CF4} {DAD5FC6A-8CA0-43AC-87E1-032DFBD6B02A} = {3F260A77-B6C9-97FD-1304-4B34DA936CF4} {8C658E1E-83C8-4127-B8BF-27A638A45DDD} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9} - {B16AC373-3DA8-4505-9510-110347CD635D} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {A5E6193C-8431-4C6E-B674-682CB41EAA0C} = {4F381919-F1BE-47D8-8558-3187ED04A84F} {E9A74E0C-BC02-4DDD-A487-89847EDF8026} = {4F381919-F1BE-47D8-8558-3187ED04A84F} + {801C9CE4-53AF-D2DB-E0D6-9A6BB47E9654} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/samples/Demos/ModelContextProtocol/McpDotNetExtensions.cs b/dotnet/samples/Demos/ModelContextProtocol/McpDotNetExtensions.cs deleted file mode 100644 index d8814bdcd695..000000000000 --- a/dotnet/samples/Demos/ModelContextProtocol/McpDotNetExtensions.cs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using McpDotNet.Client; -using McpDotNet.Configuration; -using McpDotNet.Protocol.Types; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel; - -namespace ModelContextProtocol; - -/// -/// Extension methods for McpDotNet -/// -internal static class McpDotNetExtensions -{ - /// - /// Retrieve an instance configured to connect to a GitHub server running on stdio. - /// - internal static async Task GetGitHubToolsAsync() - { - McpClientOptions options = new() - { - ClientInfo = new() { Name = "GitHub", Version = "1.0.0" } - }; - - var config = new McpServerConfig - { - Id = "github", - Name = "GitHub", - TransportType = "stdio", - TransportOptions = new Dictionary - { - ["command"] = "npx", - ["arguments"] = "-y @modelcontextprotocol/server-github", - } - }; - - var factory = new McpClientFactory( - [config], - options, - NullLoggerFactory.Instance - ); - - return await factory.GetClientAsync(config.Id).ConfigureAwait(false); - } - - /// - /// Map the tools exposed on this to a collection of instances for use with the Semantic Kernel. - /// - internal static async Task> MapToFunctionsAsync(this IMcpClient mcpClient) - { - var tools = await mcpClient.ListToolsAsync().ConfigureAwait(false); - return tools.Tools.Select(t => t.ToKernelFunction(mcpClient)).ToList(); - } - - #region private - private static KernelFunction ToKernelFunction(this Tool tool, IMcpClient mcpClient) - { - async Task InvokeToolAsync(Kernel kernel, KernelFunction function, KernelArguments arguments, CancellationToken cancellationToken) - { - try - { - // Convert arguments to dictionary format expected by mcpdotnet - Dictionary mcpArguments = []; - foreach (var arg in arguments) - { - if (arg.Value is not null) - { - mcpArguments[arg.Key] = function.ToArgumentValue(arg.Key, arg.Value); - } - } - - // Call the tool through mcpdotnet - var result = await mcpClient.CallToolAsync( - tool.Name, - mcpArguments, - cancellationToken: cancellationToken - ).ConfigureAwait(false); - - // Extract the text content from the result - return string.Join("\n", result.Content - .Where(c => c.Type == "text") - .Select(c => c.Text)); - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error invoking tool '{tool.Name}': {ex.Message}"); - - // Rethrowing to allow the kernel to handle the exception - throw; - } - } - - return KernelFunctionFactory.CreateFromMethod( - method: InvokeToolAsync, - functionName: tool.Name, - description: tool.Description, - parameters: tool.ToParameters(), - returnParameter: ToReturnParameter() - ); - } - - private static object ToArgumentValue(this KernelFunction function, string name, object value) - { - var parameter = function.Metadata.Parameters.FirstOrDefault(p => p.Name == name); - return parameter?.ParameterType switch - { - Type t when Nullable.GetUnderlyingType(t) == typeof(int) => Convert.ToInt32(value), - Type t when Nullable.GetUnderlyingType(t) == typeof(double) => Convert.ToDouble(value), - Type t when Nullable.GetUnderlyingType(t) == typeof(bool) => Convert.ToBoolean(value), - Type t when t == typeof(List) => (value as IEnumerable)?.ToList(), - Type t when t == typeof(Dictionary) => (value as Dictionary)?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), - _ => value, - } ?? value; - } - - private static List? ToParameters(this Tool tool) - { - var inputSchema = tool.InputSchema; - var properties = inputSchema?.Properties; - if (properties == null) - { - return null; - } - - HashSet requiredProperties = new(inputSchema!.Required ?? []); - return properties.Select(kvp => - new KernelParameterMetadata(kvp.Key) - { - Description = kvp.Value.Description, - ParameterType = ConvertParameterDataType(kvp.Value, requiredProperties.Contains(kvp.Key)), - IsRequired = requiredProperties.Contains(kvp.Key) - }).ToList(); - } - - private static KernelReturnParameterMetadata? ToReturnParameter() - { - return new KernelReturnParameterMetadata() - { - ParameterType = typeof(string), - }; - } - private static Type ConvertParameterDataType(JsonSchemaProperty property, bool required) - { - var type = property.Type switch - { - "string" => typeof(string), - "integer" => typeof(int), - "number" => typeof(double), - "boolean" => typeof(bool), - "array" => typeof(List), - "object" => typeof(Dictionary), - _ => typeof(object) - }; - - return !required && type.IsValueType ? typeof(Nullable<>).MakeGenericType(type) : type; - } - #endregion -} diff --git a/dotnet/samples/Demos/ModelContextProtocol/ModelContextProtocol.csproj b/dotnet/samples/Demos/ModelContextProtocolPlugin/ModelContextProtocolPlugin.csproj similarity index 81% rename from dotnet/samples/Demos/ModelContextProtocol/ModelContextProtocol.csproj rename to dotnet/samples/Demos/ModelContextProtocolPlugin/ModelContextProtocolPlugin.csproj index d509495b6882..20d6d6b81dbc 100644 --- a/dotnet/samples/Demos/ModelContextProtocol/ModelContextProtocol.csproj +++ b/dotnet/samples/Demos/ModelContextProtocolPlugin/ModelContextProtocolPlugin.csproj @@ -6,23 +6,17 @@ enable enable 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 - $(NoWarn);CA2249;CS0612 + $(NoWarn);CA2249;CS0612;SKEXP0001;VSTHRD111;CA2007 - + - - - - Always - - diff --git a/dotnet/samples/Demos/ModelContextProtocol/Program.cs b/dotnet/samples/Demos/ModelContextProtocolPlugin/Program.cs similarity index 52% rename from dotnet/samples/Demos/ModelContextProtocol/Program.cs rename to dotnet/samples/Demos/ModelContextProtocolPlugin/Program.cs index 6dfd542b6ec7..37a7adf95c31 100644 --- a/dotnet/samples/Demos/ModelContextProtocol/Program.cs +++ b/dotnet/samples/Demos/ModelContextProtocolPlugin/Program.cs @@ -5,48 +5,54 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; -using ModelContextProtocol; +using ModelContextProtocol.Client; var config = new ConfigurationBuilder() .AddUserSecrets() .AddEnvironmentVariables() .Build(); -// Prepare and build kernel -var builder = Kernel.CreateBuilder(); -builder.Services.AddLogging(c => c.AddDebug().SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace)); - -if (config["OpenAI:ApiKey"] is not null) -{ - builder.Services.AddOpenAIChatCompletion( - serviceId: "openai", - modelId: config["OpenAI:ChatModelId"] ?? "gpt-4o", - apiKey: config["OpenAI:ApiKey"]!); -} -else +if (config["OpenAI:ApiKey"] is not { } apiKey) { Console.Error.WriteLine("Please provide a valid OpenAI:ApiKey to run this sample. See the associated README.md for more details."); return; } -Kernel kernel = builder.Build(); - // Create an MCPClient for the GitHub server -var mcpClient = await McpDotNetExtensions.GetGitHubToolsAsync().ConfigureAwait(false); +await using var mcpClient = await McpClientFactory.CreateAsync( + new() + { + Id = "github", + Name = "GitHub", + TransportType = "stdio", + TransportOptions = new Dictionary + { + ["command"] = "npx", + ["arguments"] = "-y @modelcontextprotocol/server-github", + } + }, + new() { ClientInfo = new() { Name = "GitHub", Version = "1.0.0" } }).ConfigureAwait(false); // Retrieve the list of tools available on the GitHub server -var tools = await mcpClient.ListToolsAsync().ConfigureAwait(false); -foreach (var tool in tools.Tools) +var tools = await mcpClient.GetAIFunctionsAsync().ConfigureAwait(false); +foreach (var tool in tools) { Console.WriteLine($"{tool.Name}: {tool.Description}"); } -// Add the MCP tools as Kernel functions -var functions = await mcpClient.MapToFunctionsAsync().ConfigureAwait(false); -kernel.Plugins.AddFromFunctions("GitHub", functions); +// Prepare and build kernel with the MCP tools as Kernel functions +var builder = Kernel.CreateBuilder(); +builder.Services + .AddLogging(c => c.AddDebug().SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace)) + .AddOpenAIChatCompletion( + serviceId: "openai", + modelId: config["OpenAI:ChatModelId"] ?? "gpt-4o-mini", + apiKey: apiKey); +Kernel kernel = builder.Build(); +kernel.Plugins.AddFromFunctions("GitHub", tools.Select(KernelFunction.FromAIFunction)); // Enable automatic function calling -var executionSettings = new OpenAIPromptExecutionSettings +OpenAIPromptExecutionSettings executionSettings = new() { Temperature = 0, FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() diff --git a/dotnet/samples/Demos/ModelContextProtocol/README.md b/dotnet/samples/Demos/ModelContextProtocolPlugin/README.md similarity index 78% rename from dotnet/samples/Demos/ModelContextProtocol/README.md rename to dotnet/samples/Demos/ModelContextProtocolPlugin/README.md index 2592fd10f7aa..1520fc704aa6 100644 --- a/dotnet/samples/Demos/ModelContextProtocol/README.md +++ b/dotnet/samples/Demos/ModelContextProtocolPlugin/README.md @@ -6,11 +6,9 @@ MCP is an open protocol that standardizes how applications provide context to LL For for information on Model Context Protocol (MCP) please refer to the [documentation](https://modelcontextprotocol.io/introduction). -This sample uses [mcpdotnet](https://www.nuget.org/packages/mcpdotnet) is heavily influenced by the [samples](https://github.com/PederHP/mcpdotnet/tree/main/samples) from that repository. - The sample shows: -1. How to connect to an MCP Server using [mcpdotnet](https://www.nuget.org/packages/mcpdotnet) +1. How to connect to an MCP Server using [ModelContextProtocol](https://www.nuget.org/packages/ModelContextProtocol) 2. Retrieve the list of tools the MCP Server makes available 3. Convert the MCP tools to Semantic Kernel functions so they can be added to a Kernel instance 4. Invoke the tools from Semantic Kernel using function calling @@ -24,7 +22,7 @@ If you have set up those credentials as secrets within Secret Manager or through ### To set your secrets with Secret Manager ```text -cd dotnet/samples/Demos/ModelContextProtocol +cd dotnet/samples/Demos/ModelContextProtocolPlugin dotnet user-secrets init diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AIFunctionKernelFunction.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AIFunctionKernelFunction.cs index 7ab32b31b869..368513a41dfd 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AIFunctionKernelFunction.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AIFunctionKernelFunction.cs @@ -5,6 +5,7 @@ using System.ComponentModel; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -13,12 +14,6 @@ namespace Microsoft.SemanticKernel.ChatCompletion; /// Provides a that wraps an . -/// -/// The implementation of only manufactures these to pass along to the underlying -/// with autoInvoke:false, which means the -/// implementation shouldn't be invoking these functions at all. As such, the and -/// methods both unconditionally throw, even though they could be implemented. -/// internal sealed class AIFunctionKernelFunction : KernelFunction { private readonly AIFunction _aiFunction; @@ -50,16 +45,18 @@ public override KernelFunction Clone(string pluginName) return new AIFunctionKernelFunction(this, pluginName); } - protected override ValueTask InvokeCoreAsync(Kernel kernel, KernelArguments arguments, CancellationToken cancellationToken) + protected override async ValueTask InvokeCoreAsync( + Kernel kernel, KernelArguments arguments, CancellationToken cancellationToken) { - // This should never be invoked, as instances are always passed with autoInvoke:false. - throw new NotSupportedException(); + object? result = await this._aiFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false); + return new FunctionResult(this, result); } - protected override IAsyncEnumerable InvokeStreamingCoreAsync(Kernel kernel, KernelArguments arguments, CancellationToken cancellationToken) + protected override async IAsyncEnumerable InvokeStreamingCoreAsync( + Kernel kernel, KernelArguments arguments, [EnumeratorCancellation] CancellationToken cancellationToken) { - // This should never be invoked, as instances are always passed with autoInvoke:false. - throw new NotSupportedException(); + object? result = await this._aiFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false); + yield return (TResult)result!; } private static IReadOnlyList MapParameterMetadata(AIFunction aiFunction) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceChatClient.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceChatClient.cs index 45ed58f85be2..58c2830c56b5 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceChatClient.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceChatClient.cs @@ -195,7 +195,7 @@ public void Dispose() if (options.Tools is { Count: > 0 }) { - var functions = options.Tools.OfType().Select(f => new AIFunctionKernelFunction(f)); + var functions = options.Tools.OfType().Select(KernelFunction.FromAIFunction); settings.FunctionChoiceBehavior = options.ToolMode is null or AutoChatToolMode ? FunctionChoiceBehavior.Auto(functions, autoInvoke: false) : options.ToolMode is RequiredChatToolMode { RequiredFunctionName: null } ? FunctionChoiceBehavior.Required(functions, autoInvoke: false) : diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs index a0a425aca1ec..5f34cdcea313 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Functions; @@ -515,6 +516,16 @@ public AIFunction AsAIFunction(Kernel? kernel = null) return new KernelAIFunction(this, kernel); } + /// Creates an for this . + /// The instance to wrap in a . + /// An instance of that, when invoked, will in turn invoke the current . + [Experimental("SKEXP0001")] + public static KernelFunction FromAIFunction(AIFunction aiFunction) + { + Verify.NotNull(aiFunction); + return new AIFunctionKernelFunction(aiFunction); + } + /// An wrapper around a . private sealed class KernelAIFunction : AIFunction { From 9c82d7f45ca561aee83a790b9100936188e33cf6 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 25 Mar 2025 13:29:05 +0000 Subject: [PATCH 2/4] Fix AsChatClientNonStreamingToolsPropagated unit test --- .../AI/ServiceConversionExtensionsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/ServiceConversionExtensionsTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/ServiceConversionExtensionsTests.cs index 1750ed9846ed..2e25bb79f099 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/ServiceConversionExtensionsTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/ServiceConversionExtensionsTests.cs @@ -342,7 +342,7 @@ .. KernelPluginFactory.CreateFromFunctions("NiftyPlugin", foreach (var f in config.Functions!) { - await Assert.ThrowsAsync(async () => await f.InvokeAsync(new())!); + await f.InvokeAsync(new()); } } @@ -351,7 +351,7 @@ private sealed class NopAIFunction(string name) : AIFunction public override string Name => name; protected override Task InvokeCoreAsync(IEnumerable> arguments, CancellationToken cancellationToken) { - throw new FormatException(); + return Task.FromResult(null); } } From 84426fbc15293a44168d17df6d7463d175ea89f0 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 25 Mar 2025 14:06:58 +0000 Subject: [PATCH 3/4] Add AsKernelFunction extension method. --- .../ModelContextProtocolPlugin/Program.cs | 2 +- .../AI/ChatCompletion/AIFunctionExtensions.cs | 25 +++++++++++++ .../ChatCompletionServiceChatClient.cs | 2 +- .../Functions/KernelFunction.cs | 11 ------ .../Functions/AIFunctionExtensionsTests.cs | 36 +++++++++++++++++++ 5 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AIFunctionExtensions.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/Functions/AIFunctionExtensionsTests.cs diff --git a/dotnet/samples/Demos/ModelContextProtocolPlugin/Program.cs b/dotnet/samples/Demos/ModelContextProtocolPlugin/Program.cs index 37a7adf95c31..b43d2c5bf15a 100644 --- a/dotnet/samples/Demos/ModelContextProtocolPlugin/Program.cs +++ b/dotnet/samples/Demos/ModelContextProtocolPlugin/Program.cs @@ -49,7 +49,7 @@ modelId: config["OpenAI:ChatModelId"] ?? "gpt-4o-mini", apiKey: apiKey); Kernel kernel = builder.Build(); -kernel.Plugins.AddFromFunctions("GitHub", tools.Select(KernelFunction.FromAIFunction)); +kernel.Plugins.AddFromFunctions("GitHub", tools.Select(aiFunction => aiFunction.AsKernelFunction())); // Enable automatic function calling OpenAIPromptExecutionSettings executionSettings = new() diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AIFunctionExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AIFunctionExtensions.cs new file mode 100644 index 000000000000..abf08ba2ca29 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AIFunctionExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel; + +/// +/// Provides extension methods for . +/// +public static class AIFunctionExtensions +{ + /// + /// Converts an to a . + /// + /// The AI function to convert. + /// The converted . + [Experimental("SKEXP0001")] + public static KernelFunction AsKernelFunction(this AIFunction aiFunction) + { + Verify.NotNull(aiFunction); + return new AIFunctionKernelFunction(aiFunction); + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceChatClient.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceChatClient.cs index 58c2830c56b5..da55fa60cee7 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceChatClient.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceChatClient.cs @@ -195,7 +195,7 @@ public void Dispose() if (options.Tools is { Count: > 0 }) { - var functions = options.Tools.OfType().Select(KernelFunction.FromAIFunction); + var functions = options.Tools.OfType().Select(aiFunction => aiFunction.AsKernelFunction()); settings.FunctionChoiceBehavior = options.ToolMode is null or AutoChatToolMode ? FunctionChoiceBehavior.Auto(functions, autoInvoke: false) : options.ToolMode is RequiredChatToolMode { RequiredFunctionName: null } ? FunctionChoiceBehavior.Required(functions, autoInvoke: false) : diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs index 5f34cdcea313..a0a425aca1ec 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs @@ -15,7 +15,6 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Functions; @@ -516,16 +515,6 @@ public AIFunction AsAIFunction(Kernel? kernel = null) return new KernelAIFunction(this, kernel); } - /// Creates an for this . - /// The instance to wrap in a . - /// An instance of that, when invoked, will in turn invoke the current . - [Experimental("SKEXP0001")] - public static KernelFunction FromAIFunction(AIFunction aiFunction) - { - Verify.NotNull(aiFunction); - return new AIFunctionKernelFunction(aiFunction); - } - /// An wrapper around a . private sealed class KernelAIFunction : AIFunction { diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/AIFunctionExtensionsTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/AIFunctionExtensionsTests.cs new file mode 100644 index 000000000000..f6a88f267b58 --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/AIFunctionExtensionsTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; +using Microsoft.Extensions.AI; +using Xunit; +using Microsoft.SemanticKernel; + +namespace SemanticKernel.UnitTests.Functions; + +public class AIFunctionExtensionsTests +{ + [Fact] + public void ItShouldCreateKernelFunctionFromAIFunction() + { + // Arrange + AIFunction aiFunction = new TestAIFunction("TestFunction"); + + // Act + KernelFunction kernelFunction = aiFunction.AsKernelFunction(); + + // Assert + Assert.Equal("TestFunction", kernelFunction.Name); + } + + private sealed class TestAIFunction(string name) : AIFunction + { + public override string Name => name; + + protected override Task InvokeCoreAsync(IEnumerable> arguments, CancellationToken cancellationToken) + { + return Task.FromResult(null); + } + } +} From ecf94e081635e9205cca9d44f7830ddcbf01e9f4 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 25 Mar 2025 14:12:08 +0000 Subject: [PATCH 4/4] sort usings --- .../Functions/AIFunctionExtensionsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/AIFunctionExtensionsTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/AIFunctionExtensionsTests.cs index f6a88f267b58..3182e189c2ad 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/AIFunctionExtensionsTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/AIFunctionExtensionsTests.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Threading.Tasks; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.AI; -using Xunit; using Microsoft.SemanticKernel; +using Xunit; namespace SemanticKernel.UnitTests.Functions;