diff --git a/dotnet/samples/Concepts/Functions/MethodFunctions_Advanced.cs b/dotnet/samples/Concepts/Functions/MethodFunctions_Advanced.cs index 6583e2dee7e2..7bc3f2bc02cf 100644 --- a/dotnet/samples/Concepts/Functions/MethodFunctions_Advanced.cs +++ b/dotnet/samples/Concepts/Functions/MethodFunctions_Advanced.cs @@ -2,27 +2,28 @@ using System.ComponentModel; using System.Globalization; +using System.Reflection; using System.Text.Json; using Microsoft.SemanticKernel; namespace Functions; -// This example shows different ways how to define and execute method functions using custom and primitive types. +/// +/// These samples show advanced usage of method functions. +/// public class MethodFunctions_Advanced(ITestOutputHelper output) : BaseTest(output) { - #region Method Functions Chaining - /// /// This example executes Function1, which in turn executes Function2. /// [Fact] - public async Task MethodFunctionsChainingAsync() + public async Task MethodFunctionsChaining() { Console.WriteLine("Running Method Function Chaining example..."); var kernel = new Kernel(); - var functions = kernel.ImportPluginFromType(); + var functions = kernel.ImportPluginFromType(); var customType = await kernel.InvokeAsync(functions["Function1"]); @@ -31,11 +32,28 @@ public async Task MethodFunctionsChainingAsync() } /// - /// Plugin example with two method functions, where one function is called from another. + /// This example shows how to access the custom attribute the underlying method wrapped by Kernel Function is annotated with. /// - private sealed class FunctionsChainingPlugin + [Fact] + public async Task AccessUnderlyingMethodAttributes() + { + // Import the plugin containing the method with the InvocationSettingsAttribute custom attribute + var kernel = new Kernel(); + + var functions = kernel.ImportPluginFromType(); + + // Get the kernel function wrapping the method with the InvocationSettingsAttribute + var kernelFunction = functions[nameof(Plugin.FunctionWithInvocationSettingsAttribute)]; + + // Access the custom attribute the underlying method is annotated with + var invocationSettingsAttribute = kernelFunction.UnderlyingMethod!.GetCustomAttribute(); + + Console.WriteLine($"Priority: {invocationSettingsAttribute?.Priority}"); + } + + private sealed class Plugin { - private const string PluginName = nameof(FunctionsChainingPlugin); + private const string PluginName = nameof(Plugin); [KernelFunction] public async Task Function1Async(Kernel kernel) @@ -59,11 +77,12 @@ public static MyCustomType Function2() Text = "From Function2" }; } - } - - #endregion - #region Custom Type + [KernelFunction, InvocationSettingsAttribute(priority: Priority.High)] + public static void FunctionWithInvocationSettingsAttribute() + { + } + } /// /// In order to use custom types, should be specified, @@ -110,5 +129,20 @@ private sealed class MyCustomTypeConverter : TypeConverter } } - #endregion + [AttributeUsage(AttributeTargets.Method)] + private sealed class InvocationSettingsAttribute : Attribute + { + public InvocationSettingsAttribute(Priority priority = Priority.Normal) + { + this.Priority = priority; + } + + public Priority Priority { get; } + } + + private enum Priority + { + Normal, + High, + } } diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs index a0a425aca1ec..a77a0f782340 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs @@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; @@ -98,6 +99,15 @@ public abstract class KernelFunction /// public IReadOnlyDictionary? ExecutionSettings { get; } + /// + /// Gets the underlying that this function might be wrapping. + /// + /// + /// Provides additional metadata on the function and its signature. Implementations not wrapping .NET methods may return null. + /// + [Experimental("SKEXP0001")] + public MethodInfo? UnderlyingMethod { get; internal init; } + /// /// Initializes a new instance of the class. /// diff --git a/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs b/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs index 6a8594b3ff0c..49991ad39c5a 100644 --- a/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs +++ b/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs @@ -141,6 +141,7 @@ public static KernelFunction Create( MethodDetails methodDetails = GetMethodDetails(options?.FunctionName, method, target); var result = new KernelFunctionFromMethod( + method, methodDetails.Function, methodDetails.Name, options?.Description ?? methodDetails.Description, @@ -181,6 +182,7 @@ public static KernelFunction Create( MethodDetails methodDetails = GetMethodDetails(options?.FunctionName, method, jsonSerializerOptions, target); var result = new KernelFunctionFromMethod( + method, methodDetails.Function, methodDetails.Name, options?.Description ?? methodDetails.Description, @@ -275,6 +277,7 @@ public static KernelFunctionMetadata CreateMetadata( MethodDetails methodDetails = GetMethodDetails(options?.FunctionName, method, null); var result = new KernelFunctionFromMethod( + method, methodDetails.Function, methodDetails.Name, options?.Description ?? methodDetails.Description, @@ -307,6 +310,7 @@ public static KernelFunctionMetadata CreateMetadata( MethodDetails methodDetails = GetMethodDetails(options?.FunctionName, method, jsonSerializerOptions, target: null); var result = new KernelFunctionFromMethod( + method, methodDetails.Function, methodDetails.Name, options?.Description ?? methodDetails.Description, @@ -382,6 +386,7 @@ public override KernelFunction Clone(string pluginName) if (base.JsonSerializerOptions is not null) { return new KernelFunctionFromMethod( + this.UnderlyingMethod!, this._function, this.Name, pluginName, @@ -397,6 +402,7 @@ public override KernelFunction Clone(string pluginName) KernelFunctionFromMethod Clone() { return new KernelFunctionFromMethod( + this.UnderlyingMethod!, this._function, this.Name, pluginName, @@ -424,17 +430,19 @@ private record struct MethodDetails(string Name, string Description, Implementat [RequiresUnreferencedCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")] [RequiresDynamicCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")] private KernelFunctionFromMethod( + MethodInfo method, ImplementationFunc implementationFunc, string functionName, string description, IReadOnlyList parameters, KernelReturnParameterMetadata returnParameter, ReadOnlyDictionary? additionalMetadata = null) : - this(implementationFunc, functionName, null, description, parameters, returnParameter, additionalMetadata) + this(method, implementationFunc, functionName, null, description, parameters, returnParameter, additionalMetadata) { } private KernelFunctionFromMethod( + MethodInfo method, ImplementationFunc implementationFunc, string functionName, string description, @@ -442,13 +450,14 @@ private KernelFunctionFromMethod( KernelReturnParameterMetadata returnParameter, JsonSerializerOptions jsonSerializerOptions, ReadOnlyDictionary? additionalMetadata = null) : - this(implementationFunc, functionName, null, description, parameters, returnParameter, jsonSerializerOptions, additionalMetadata) + this(method, implementationFunc, functionName, null, description, parameters, returnParameter, jsonSerializerOptions, additionalMetadata) { } [RequiresUnreferencedCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")] [RequiresDynamicCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")] private KernelFunctionFromMethod( + MethodInfo method, ImplementationFunc implementationFunc, string functionName, string? pluginName, @@ -461,9 +470,11 @@ private KernelFunctionFromMethod( Verify.ValidFunctionName(functionName); this._function = implementationFunc; + this.UnderlyingMethod = method; } private KernelFunctionFromMethod( + MethodInfo method, ImplementationFunc implementationFunc, string functionName, string? pluginName, @@ -477,6 +488,7 @@ private KernelFunctionFromMethod( Verify.ValidFunctionName(functionName); this._function = implementationFunc; + this.UnderlyingMethod = method; } [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "This method is AOT save.")] diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionFromMethodTests2.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionFromMethodTests2.cs index 66264fe6bb35..f2800220d8a1 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionFromMethodTests2.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionFromMethodTests2.cs @@ -288,6 +288,24 @@ public void ItMakesProvidedExtensionPropertiesAvailableViaMetadataWhenConstructe Assert.Equal("value1", func.Metadata.AdditionalProperties["key1"]); } + [Fact] + public void ItShouldExposeUnderlyingMethod() + { + // Arrange + var target = new LocalExamplePlugin(); + + var methodInfo = target.GetType().GetMethod(nameof(LocalExamplePlugin.FunctionWithCustomAttribute))!; + + var kernelFunction = KernelFunctionFactory.CreateFromMethod(methodInfo, target); + + // Assert + Assert.NotNull(kernelFunction.UnderlyingMethod); + + Assert.Equal(methodInfo, kernelFunction.UnderlyingMethod); + + Assert.NotNull(kernelFunction.UnderlyingMethod.GetCustomAttribute()); + } + private interface IExampleService; private sealed class ExampleService : IExampleService; @@ -466,6 +484,11 @@ public string WithPrimitives( { return string.Empty; } + + [KernelFunction, CustomAttribute] + public void FunctionWithCustomAttribute() + { + } } private sealed class GenericPlugin @@ -479,4 +502,9 @@ private sealed class GenericPlugin [KernelFunction] public Task GetValue3Async(T input) => Task.FromResult(input); } + + [AttributeUsage(AttributeTargets.Method)] + private sealed class CustomAttribute : Attribute + { + } }