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: New CreateFromType and CreateMetadataFromType methods #9232

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
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,66 @@ public static KernelFunction Create(
return result;
}

/// <summary>
/// Creates a <see cref="KernelFunctionMetadata"/> instance for a method, specified via an <see cref="MethodInfo"/> instance.
/// </summary>
/// <param name="method">The method to be represented via the created <see cref="KernelFunction"/>.</param>
/// <param name="functionName">The name to use for the function. If null, it will default to one derived from the method represented by <paramref name="method"/>.</param>
/// <param name="description">The description to use for the function. If null, it will default to one derived from the method represented by <paramref name="method"/>, if possible (e.g. via a <see cref="DescriptionAttribute"/> on the method).</param>
/// <param name="parameters">Optional parameter descriptions. If null, it will default to one derived from the method represented by <paramref name="method"/>.</param>
/// <param name="returnParameter">Optional return parameter description. If null, it will default to one derived from the method represented by <paramref name="method"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
/// <returns>The created <see cref="KernelFunction"/> wrapper for <paramref name="method"/>.</returns>
[Experimental("SKEXP0001")]
public static KernelFunctionMetadata CreateMetadata(
MethodInfo method,
string? functionName = null,
string? description = null,
IEnumerable<KernelParameterMetadata>? parameters = null,
KernelReturnParameterMetadata? returnParameter = null,
ILoggerFactory? loggerFactory = null)
=> CreateMetadata(
method,
new KernelFunctionFromMethodOptions
{
FunctionName = functionName,
Description = description,
Parameters = parameters,
ReturnParameter = returnParameter,
LoggerFactory = loggerFactory
});

/// <summary>
/// Creates a <see cref="KernelFunctionMetadata"/> instance for a method, specified via an <see cref="MethodInfo"/> instance.
/// </summary>
/// <param name="method">The method to be represented via the created <see cref="KernelFunction"/>.</param>
/// <param name="options">Optional function creation options.</param>
/// <returns>The created <see cref="KernelFunction"/> wrapper for <paramref name="method"/>.</returns>
[Experimental("SKEXP0001")]
public static KernelFunctionMetadata CreateMetadata(
markwallace-microsoft marked this conversation as resolved.
Show resolved Hide resolved
MethodInfo method,
KernelFunctionFromMethodOptions? options = default)
{
Verify.NotNull(method);

MethodDetails methodDetails = GetMethodDetails(options?.FunctionName, method, null);
var result = new KernelFunctionFromMethod(
methodDetails.Function,
methodDetails.Name,
options?.Description ?? methodDetails.Description,
options?.Parameters?.ToList() ?? methodDetails.Parameters,
options?.ReturnParameter ?? methodDetails.ReturnParameter,
options?.AdditionalMetadata);

if (options?.LoggerFactory?.CreateLogger(method.DeclaringType ?? typeof(KernelFunctionFromPrompt)) is ILogger logger &&
logger.IsEnabled(LogLevel.Trace))
{
logger.LogTrace("Created KernelFunctionMetadata '{Name}' for '{MethodName}'", result.Name, method.Name);
}

return result.Metadata;
}

/// <inheritdoc/>
protected override ValueTask<FunctionResult> InvokeCoreAsync(
Kernel kernel,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.Extensions.Logging;

namespace Microsoft.SemanticKernel;

/// <summary>
/// Provides factory methods for creating collections of <see cref="KernelFunctionMetadata"/>, such as
/// those backed by a prompt to be submitted to an LLM or those backed by a .NET method.
/// </summary>
[Experimental("SKEXP0001")]
public static class KernelFunctionMetadataFactory
{
/// <summary>
/// Creates a <see cref="KernelFunctionMetadata"/> enumeration for a method, specified via an <see cref="MethodInfo"/> instance.
/// </summary>
/// <param name="instanceType">Specifies the type of the object to extract <see cref="KernelFunctionMetadata"/> for.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
/// <returns>A <see cref="KernelPlugin"/> containing <see cref="KernelFunction"/>s for all relevant members of <paramref name="instanceType"/>.</returns>
/// <remarks>
/// Methods decorated with <see cref="KernelFunctionAttribute"/> will be included in the plugin.
/// Attributed methods must all have different names; overloads are not supported.
/// </remarks>
public static IEnumerable<KernelFunctionMetadata> CreateFromType(Type instanceType, ILoggerFactory? loggerFactory = null)
{
Verify.NotNull(instanceType);

MethodInfo[] methods = instanceType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);

// Filter out non-KernelFunctions and fail if two functions have the same name (with or without the same casing).
var functionMetadata = new List<KernelFunctionMetadata>();
KernelFunctionFromMethodOptions options = new();
foreach (MethodInfo method in methods)
{
if (method.GetCustomAttribute<KernelFunctionAttribute>() is not null)
{
functionMetadata.Add(KernelFunctionFromMethod.CreateMetadata(method, loggerFactory: loggerFactory));
}
}
if (functionMetadata.Count == 0)
{
throw new ArgumentException($"The {instanceType} instance doesn't implement any [KernelFunction]-attributed methods.");
}

return functionMetadata;
}
}
24 changes: 24 additions & 0 deletions dotnet/src/SemanticKernel.Core/Functions/KernelPluginFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
Expand Down Expand Up @@ -36,6 +37,29 @@ public static KernelPlugin CreateFromType<T>(string? pluginName = null, IService
return CreateFromObject(ActivatorUtilities.CreateInstance<T>(serviceProvider)!, pluginName, serviceProvider?.GetService<ILoggerFactory>());
}

/// <summary>Creates a plugin that wraps a new instance of the specified type <paramref name="instanceType"/>.</summary>
/// <param name="instanceType">
/// Specifies the type of the object to wrap.
/// </param>
/// <param name="pluginName">
/// Name of the plugin for function collection and prompt templates. If the value is null, a plugin name is derived from the <paramref name="instanceType"/>.
/// </param>
/// <param name="serviceProvider">
/// The <see cref="IServiceProvider"/> to use for resolving any required services, such as an <see cref="ILoggerFactory"/>
/// and any services required to satisfy a constructor on <paramref name="instanceType"/>.
/// </param>
/// <returns>A <see cref="KernelPlugin"/> containing <see cref="KernelFunction"/>s for all relevant members of <paramref name="instanceType"/>.</returns>
/// <remarks>
/// Methods decorated with <see cref="KernelFunctionAttribute"/> will be included in the plugin.
/// Attributed methods must all have different names; overloads are not supported.
/// </remarks>
[Experimental("SKEXP0001")]
public static KernelPlugin CreateFromType(Type instanceType, string? pluginName = null, IServiceProvider? serviceProvider = null)
{
serviceProvider ??= EmptyServiceProvider.Instance;
return CreateFromObject(ActivatorUtilities.CreateInstance(serviceProvider, instanceType)!, pluginName, serviceProvider?.GetService<ILoggerFactory>());
}

/// <summary>Creates a plugin that wraps the specified target object.</summary>
/// <param name="target">The instance of the class to be wrapped.</param>
/// <param name="pluginName">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.ComponentModel;
using System.Linq;
using Microsoft.SemanticKernel;
using Xunit;

namespace SemanticKernel.UnitTests.Functions;

public class KernelFunctionMetadataFactoryTests
{
[Fact]
public void ItCanCreateFromType()
{
// Arrange
var instanceType = typeof(MyKernelFunctions);

// Act
var functionMetadata = KernelFunctionMetadataFactory.CreateFromType(instanceType);

// Assert
Assert.NotNull(functionMetadata);
Assert.Equal(3, functionMetadata.Count<KernelFunctionMetadata>());
Assert.Contains(functionMetadata, f => f.Name == "Function1");
Assert.Contains(functionMetadata, f => f.Name == "Function2");
Assert.Contains(functionMetadata, f => f.Name == "Function3");
}

#region private
#pragma warning disable CA1812 // Used in test case above
private sealed class MyKernelFunctions
{
// Disallow instantiation of this class.
private MyKernelFunctions()
{
}
markwallace-microsoft marked this conversation as resolved.
Show resolved Hide resolved

[KernelFunction("Function1")]
[Description("Description for function 1.")]
public string Function1([Description("Description for parameter 1")] string param1) => $"Function1: {param1}";

[KernelFunction("Function2")]
[Description("Description for function 2.")]
public string Function2([Description("Description for parameter 1")] string param1) => $"Function2: {param1}";

[KernelFunction("Function3")]
[Description("Description for function 3.")]
public string Function3([Description("Description for parameter 1")] string param1) => $"Function3: {param1}";
}
#pragma warning restore CA1812
#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.

using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Xunit;

namespace SemanticKernel.UnitTests.Functions;
public class KernelPluginFactoryTests
{
[Fact]
public async Task ItCanCreateFromObjectAsync()
{
// Arrange
var kernel = new Kernel();
var args = new KernelArguments { { "param1", "value1" } };
var target = new MyKernelFunctions();

// Act
var plugin = KernelPluginFactory.CreateFromObject(target);
FunctionResult result = await plugin["Function1"].InvokeAsync(kernel, args);

// Assert
Assert.NotNull(plugin);
Assert.Equal(3, plugin.FunctionCount);
Assert.Equal("Function1: value1", result.Value);
}

[Fact]
public async Task ItCanCreateFromTypeUsingGenericsAsync()
{
// Arrange
var kernel = new Kernel();
var args = new KernelArguments { { "param1", "value1" } };

// Act
var plugin = KernelPluginFactory.CreateFromType<MyKernelFunctions>();
FunctionResult result = await plugin["Function1"].InvokeAsync(kernel, args);

// Assert
Assert.NotNull(plugin);
Assert.Equal(3, plugin.FunctionCount);
Assert.Equal("Function1: value1", result.Value);
}

[Fact]
public async Task ItCanCreateFromTypeAsync()
{
// Arrange
var kernel = new Kernel();
var args = new KernelArguments { { "param1", "value1" } };
var instanceType = typeof(MyKernelFunctions);

// Act
var plugin = KernelPluginFactory.CreateFromType(instanceType);
FunctionResult result = await plugin["Function1"].InvokeAsync(kernel, args);

// Assert
Assert.NotNull(plugin);
Assert.Equal(3, plugin.FunctionCount);
Assert.Equal("Function1: value1", result.Value);
}

#region private
private sealed class MyKernelFunctions
{
[KernelFunction("Function1")]
[Description("Description for function 1.")]
public string Function1([Description("Description for parameter 1")] string param1) => $"Function1: {param1}";

[KernelFunction("Function2")]
[Description("Description for function 2.")]
public string Function2([Description("Description for parameter 1")] string param1) => $"Function2: {param1}";

[KernelFunction("Function3")]
[Description("Description for function 3.")]
public string Function3([Description("Description for parameter 1")] string param1) => $"Function3: {param1}";
}
#endregion
}
Loading