Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.Net: Expose underlying method from kernel function #11378

Merged
merged 14 commits into from
Apr 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 47 additions & 13 deletions dotnet/samples/Concepts/Functions/MethodFunctions_Advanced.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// <summary>
/// These samples show advanced usage of method functions.
/// </summary>
public class MethodFunctions_Advanced(ITestOutputHelper output) : BaseTest(output)
{
#region Method Functions Chaining

/// <summary>
/// This example executes Function1, which in turn executes Function2.
/// </summary>
[Fact]
public async Task MethodFunctionsChainingAsync()
public async Task MethodFunctionsChaining()
{
Console.WriteLine("Running Method Function Chaining example...");

var kernel = new Kernel();

var functions = kernel.ImportPluginFromType<FunctionsChainingPlugin>();
var functions = kernel.ImportPluginFromType<Plugin>();

var customType = await kernel.InvokeAsync<MyCustomType>(functions["Function1"]);

Expand All @@ -31,11 +32,28 @@ public async Task MethodFunctionsChainingAsync()
}

/// <summary>
/// Plugin example with two method functions, where one function is called from another.
/// This example shows how to access the custom <see cref="InvocationSettingsAttribute"/> attribute the underlying method wrapped by Kernel Function is annotated with.
/// </summary>
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<Plugin>();

// 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<InvocationSettingsAttribute>();

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<MyCustomType> Function1Async(Kernel kernel)
Expand All @@ -59,11 +77,12 @@ public static MyCustomType Function2()
Text = "From Function2"
};
}
}

#endregion

#region Custom Type
[KernelFunction, InvocationSettingsAttribute(priority: Priority.High)]
public static void FunctionWithInvocationSettingsAttribute()
{
}
}

/// <summary>
/// In order to use custom types, <see cref="TypeConverter"/> should be specified,
Expand Down Expand Up @@ -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,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -98,6 +99,15 @@ public abstract class KernelFunction
/// </remarks>
public IReadOnlyDictionary<string, PromptExecutionSettings>? ExecutionSettings { get; }

/// <summary>
/// Gets the underlying <see cref="MethodInfo"/> that this function might be wrapping.
/// </summary>
/// <remarks>
/// Provides additional metadata on the function and its signature. Implementations not wrapping .NET methods may return null.
/// </remarks>
[Experimental("SKEXP0001")]
public MethodInfo? UnderlyingMethod { get; internal init; }

/// <summary>
/// Initializes a new instance of the <see cref="KernelFunction"/> class.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -397,6 +402,7 @@ public override KernelFunction Clone(string pluginName)
KernelFunctionFromMethod Clone()
{
return new KernelFunctionFromMethod(
this.UnderlyingMethod!,
this._function,
this.Name,
pluginName,
Expand Down Expand Up @@ -424,31 +430,34 @@ 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<KernelParameterMetadata> parameters,
KernelReturnParameterMetadata returnParameter,
ReadOnlyDictionary<string, object?>? 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,
IReadOnlyList<KernelParameterMetadata> parameters,
KernelReturnParameterMetadata returnParameter,
JsonSerializerOptions jsonSerializerOptions,
ReadOnlyDictionary<string, object?>? 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,
Expand All @@ -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,
Expand All @@ -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.")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CustomAttribute>());
}

private interface IExampleService;

private sealed class ExampleService : IExampleService;
Expand Down Expand Up @@ -466,6 +484,11 @@ public string WithPrimitives(
{
return string.Empty;
}

[KernelFunction, CustomAttribute]
public void FunctionWithCustomAttribute()
{
}
}

private sealed class GenericPlugin<T>
Expand All @@ -479,4 +502,9 @@ private sealed class GenericPlugin<T>
[KernelFunction]
public Task<T> GetValue3Async(T input) => Task.FromResult(input);
}

[AttributeUsage(AttributeTargets.Method)]
private sealed class CustomAttribute : Attribute
{
}
}
Loading