Skip to content

Commit

Permalink
New CreateFromType and CreateMetadataFromType methods
Browse files Browse the repository at this point in the history
  • Loading branch information
markwallace-microsoft committed Oct 12, 2024
1 parent ec9233c commit c6ad569
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 3 deletions.
6 changes: 3 additions & 3 deletions dotnet/src/IntegrationTests/Processes/ProcessCycleTests.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.SemanticKernel;
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
using SemanticKernel.IntegrationTests.Agents;
using SemanticKernel.IntegrationTests.TestSettings;
using Xunit;
using SemanticKernel.IntegrationTests.Agents;

namespace SemanticKernel.IntegrationTests.Processes;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,67 @@ 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)
{
return 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>
public static KernelFunctionMetadata CreateMetadata(
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,52 @@
// 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()
{
}

[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
}

0 comments on commit c6ad569

Please sign in to comment.