Skip to content

Commit 3ec6d01

Browse files
.Net: Expose underlying method from kernel function (#11378)
### Motivation, Context and Description Currently, there is no way to access the underlying method that an instance of `KernelFunctionFromMethod` was created from or wraps. Being able to access this underlying method is useful when additional metadata is needed. For example, if the method has a custom attribute, you may need to retrieve that attribute to determine the execution path. This PR adopts the same approach as M.E.AI by exposing the native method through the `public MethodInfo? UnderlyingMethod {...}` property. This property is null by default and is only initialized for native functions represented by the `KernelFunctionFromMethod` class. Closes: #11182 --------- Co-authored-by: Roger Barreto <[email protected]>
1 parent 61c3fdc commit 3ec6d01

File tree

4 files changed

+99
-15
lines changed

4 files changed

+99
-15
lines changed

dotnet/samples/Concepts/Functions/MethodFunctions_Advanced.cs

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,28 @@
22

33
using System.ComponentModel;
44
using System.Globalization;
5+
using System.Reflection;
56
using System.Text.Json;
67
using Microsoft.SemanticKernel;
78

89
namespace Functions;
910

10-
// This example shows different ways how to define and execute method functions using custom and primitive types.
11+
/// <summary>
12+
/// These samples show advanced usage of method functions.
13+
/// </summary>
1114
public class MethodFunctions_Advanced(ITestOutputHelper output) : BaseTest(output)
1215
{
13-
#region Method Functions Chaining
14-
1516
/// <summary>
1617
/// This example executes Function1, which in turn executes Function2.
1718
/// </summary>
1819
[Fact]
19-
public async Task MethodFunctionsChainingAsync()
20+
public async Task MethodFunctionsChaining()
2021
{
2122
Console.WriteLine("Running Method Function Chaining example...");
2223

2324
var kernel = new Kernel();
2425

25-
var functions = kernel.ImportPluginFromType<FunctionsChainingPlugin>();
26+
var functions = kernel.ImportPluginFromType<Plugin>();
2627

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

@@ -31,11 +32,28 @@ public async Task MethodFunctionsChainingAsync()
3132
}
3233

3334
/// <summary>
34-
/// Plugin example with two method functions, where one function is called from another.
35+
/// This example shows how to access the custom <see cref="InvocationSettingsAttribute"/> attribute the underlying method wrapped by Kernel Function is annotated with.
3536
/// </summary>
36-
private sealed class FunctionsChainingPlugin
37+
[Fact]
38+
public async Task AccessUnderlyingMethodAttributes()
39+
{
40+
// Import the plugin containing the method with the InvocationSettingsAttribute custom attribute
41+
var kernel = new Kernel();
42+
43+
var functions = kernel.ImportPluginFromType<Plugin>();
44+
45+
// Get the kernel function wrapping the method with the InvocationSettingsAttribute
46+
var kernelFunction = functions[nameof(Plugin.FunctionWithInvocationSettingsAttribute)];
47+
48+
// Access the custom attribute the underlying method is annotated with
49+
var invocationSettingsAttribute = kernelFunction.UnderlyingMethod!.GetCustomAttribute<InvocationSettingsAttribute>();
50+
51+
Console.WriteLine($"Priority: {invocationSettingsAttribute?.Priority}");
52+
}
53+
54+
private sealed class Plugin
3755
{
38-
private const string PluginName = nameof(FunctionsChainingPlugin);
56+
private const string PluginName = nameof(Plugin);
3957

4058
[KernelFunction]
4159
public async Task<MyCustomType> Function1Async(Kernel kernel)
@@ -59,11 +77,12 @@ public static MyCustomType Function2()
5977
Text = "From Function2"
6078
};
6179
}
62-
}
63-
64-
#endregion
6580

66-
#region Custom Type
81+
[KernelFunction, InvocationSettingsAttribute(priority: Priority.High)]
82+
public static void FunctionWithInvocationSettingsAttribute()
83+
{
84+
}
85+
}
6786

6887
/// <summary>
6988
/// In order to use custom types, <see cref="TypeConverter"/> should be specified,
@@ -110,5 +129,20 @@ private sealed class MyCustomTypeConverter : TypeConverter
110129
}
111130
}
112131

113-
#endregion
132+
[AttributeUsage(AttributeTargets.Method)]
133+
private sealed class InvocationSettingsAttribute : Attribute
134+
{
135+
public InvocationSettingsAttribute(Priority priority = Priority.Normal)
136+
{
137+
this.Priority = priority;
138+
}
139+
140+
public Priority Priority { get; }
141+
}
142+
143+
private enum Priority
144+
{
145+
Normal,
146+
High,
147+
}
114148
}

dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Diagnostics.CodeAnalysis;
99
using System.Diagnostics.Metrics;
1010
using System.Linq;
11+
using System.Reflection;
1112
using System.Runtime.CompilerServices;
1213
using System.Text.Json;
1314
using System.Threading;
@@ -98,6 +99,15 @@ public abstract class KernelFunction
9899
/// </remarks>
99100
public IReadOnlyDictionary<string, PromptExecutionSettings>? ExecutionSettings { get; }
100101

102+
/// <summary>
103+
/// Gets the underlying <see cref="MethodInfo"/> that this function might be wrapping.
104+
/// </summary>
105+
/// <remarks>
106+
/// Provides additional metadata on the function and its signature. Implementations not wrapping .NET methods may return null.
107+
/// </remarks>
108+
[Experimental("SKEXP0001")]
109+
public MethodInfo? UnderlyingMethod { get; internal init; }
110+
101111
/// <summary>
102112
/// Initializes a new instance of the <see cref="KernelFunction"/> class.
103113
/// </summary>

dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ public static KernelFunction Create(
141141

142142
MethodDetails methodDetails = GetMethodDetails(options?.FunctionName, method, target);
143143
var result = new KernelFunctionFromMethod(
144+
method,
144145
methodDetails.Function,
145146
methodDetails.Name,
146147
options?.Description ?? methodDetails.Description,
@@ -181,6 +182,7 @@ public static KernelFunction Create(
181182

182183
MethodDetails methodDetails = GetMethodDetails(options?.FunctionName, method, jsonSerializerOptions, target);
183184
var result = new KernelFunctionFromMethod(
185+
method,
184186
methodDetails.Function,
185187
methodDetails.Name,
186188
options?.Description ?? methodDetails.Description,
@@ -275,6 +277,7 @@ public static KernelFunctionMetadata CreateMetadata(
275277

276278
MethodDetails methodDetails = GetMethodDetails(options?.FunctionName, method, null);
277279
var result = new KernelFunctionFromMethod(
280+
method,
278281
methodDetails.Function,
279282
methodDetails.Name,
280283
options?.Description ?? methodDetails.Description,
@@ -307,6 +310,7 @@ public static KernelFunctionMetadata CreateMetadata(
307310

308311
MethodDetails methodDetails = GetMethodDetails(options?.FunctionName, method, jsonSerializerOptions, target: null);
309312
var result = new KernelFunctionFromMethod(
313+
method,
310314
methodDetails.Function,
311315
methodDetails.Name,
312316
options?.Description ?? methodDetails.Description,
@@ -382,6 +386,7 @@ public override KernelFunction Clone(string pluginName)
382386
if (base.JsonSerializerOptions is not null)
383387
{
384388
return new KernelFunctionFromMethod(
389+
this.UnderlyingMethod!,
385390
this._function,
386391
this.Name,
387392
pluginName,
@@ -397,6 +402,7 @@ public override KernelFunction Clone(string pluginName)
397402
KernelFunctionFromMethod Clone()
398403
{
399404
return new KernelFunctionFromMethod(
405+
this.UnderlyingMethod!,
400406
this._function,
401407
this.Name,
402408
pluginName,
@@ -424,31 +430,34 @@ private record struct MethodDetails(string Name, string Description, Implementat
424430
[RequiresUnreferencedCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")]
425431
[RequiresDynamicCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")]
426432
private KernelFunctionFromMethod(
433+
MethodInfo method,
427434
ImplementationFunc implementationFunc,
428435
string functionName,
429436
string description,
430437
IReadOnlyList<KernelParameterMetadata> parameters,
431438
KernelReturnParameterMetadata returnParameter,
432439
ReadOnlyDictionary<string, object?>? additionalMetadata = null) :
433-
this(implementationFunc, functionName, null, description, parameters, returnParameter, additionalMetadata)
440+
this(method, implementationFunc, functionName, null, description, parameters, returnParameter, additionalMetadata)
434441
{
435442
}
436443

437444
private KernelFunctionFromMethod(
445+
MethodInfo method,
438446
ImplementationFunc implementationFunc,
439447
string functionName,
440448
string description,
441449
IReadOnlyList<KernelParameterMetadata> parameters,
442450
KernelReturnParameterMetadata returnParameter,
443451
JsonSerializerOptions jsonSerializerOptions,
444452
ReadOnlyDictionary<string, object?>? additionalMetadata = null) :
445-
this(implementationFunc, functionName, null, description, parameters, returnParameter, jsonSerializerOptions, additionalMetadata)
453+
this(method, implementationFunc, functionName, null, description, parameters, returnParameter, jsonSerializerOptions, additionalMetadata)
446454
{
447455
}
448456

449457
[RequiresUnreferencedCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")]
450458
[RequiresDynamicCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")]
451459
private KernelFunctionFromMethod(
460+
MethodInfo method,
452461
ImplementationFunc implementationFunc,
453462
string functionName,
454463
string? pluginName,
@@ -461,9 +470,11 @@ private KernelFunctionFromMethod(
461470
Verify.ValidFunctionName(functionName);
462471

463472
this._function = implementationFunc;
473+
this.UnderlyingMethod = method;
464474
}
465475

466476
private KernelFunctionFromMethod(
477+
MethodInfo method,
467478
ImplementationFunc implementationFunc,
468479
string functionName,
469480
string? pluginName,
@@ -477,6 +488,7 @@ private KernelFunctionFromMethod(
477488
Verify.ValidFunctionName(functionName);
478489

479490
this._function = implementationFunc;
491+
this.UnderlyingMethod = method;
480492
}
481493

482494
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "This method is AOT save.")]

dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionFromMethodTests2.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,24 @@ public void ItMakesProvidedExtensionPropertiesAvailableViaMetadataWhenConstructe
288288
Assert.Equal("value1", func.Metadata.AdditionalProperties["key1"]);
289289
}
290290

291+
[Fact]
292+
public void ItShouldExposeUnderlyingMethod()
293+
{
294+
// Arrange
295+
var target = new LocalExamplePlugin();
296+
297+
var methodInfo = target.GetType().GetMethod(nameof(LocalExamplePlugin.FunctionWithCustomAttribute))!;
298+
299+
var kernelFunction = KernelFunctionFactory.CreateFromMethod(methodInfo, target);
300+
301+
// Assert
302+
Assert.NotNull(kernelFunction.UnderlyingMethod);
303+
304+
Assert.Equal(methodInfo, kernelFunction.UnderlyingMethod);
305+
306+
Assert.NotNull(kernelFunction.UnderlyingMethod.GetCustomAttribute<CustomAttribute>());
307+
}
308+
291309
private interface IExampleService;
292310

293311
private sealed class ExampleService : IExampleService;
@@ -466,6 +484,11 @@ public string WithPrimitives(
466484
{
467485
return string.Empty;
468486
}
487+
488+
[KernelFunction, CustomAttribute]
489+
public void FunctionWithCustomAttribute()
490+
{
491+
}
469492
}
470493

471494
private sealed class GenericPlugin<T>
@@ -479,4 +502,9 @@ private sealed class GenericPlugin<T>
479502
[KernelFunction]
480503
public Task<T> GetValue3Async(T input) => Task.FromResult(input);
481504
}
505+
506+
[AttributeUsage(AttributeTargets.Method)]
507+
private sealed class CustomAttribute : Attribute
508+
{
509+
}
482510
}

0 commit comments

Comments
 (0)