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 any custom attributes on the underlying function method #8367

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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 @@ -127,6 +127,7 @@ private static KernelPlugin ImportFunctions(Kernel kernel)
(string cityName) => "12°C\nWind: 11 KMPH\nHumidity: 48%\nMostly cloudy",
"GetWeatherForCity",
"Gets the current weather for the specified city",
[],
new List<KernelParameterMetadata>
{
new("cityName") { Description = "The city name", ParameterType = string.Empty.GetType() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ private static KernelPlugin GetTestPlugin()
(string parameter1, string parameter2) => "Result1",
"MyFunction",
"Test Function",
[],
[new KernelParameterMetadata("parameter1"), new KernelParameterMetadata("parameter2")],
new KernelReturnParameterMetadata { ParameterType = typeof(string), Description = "Function Result" });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ private KernelPlugin GetTestPlugin()
(string parameter1, string parameter2) => "Result1",
"MyFunction",
"Test Function",
[],
[new KernelParameterMetadata("parameter1"), new KernelParameterMetadata("parameter2")],
new KernelReturnParameterMetadata { ParameterType = typeof(string), Description = "Function Result" });

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Experimental.Agents;
Expand Down Expand Up @@ -33,8 +34,9 @@ public static void GetToolModelFromFunction()

var requiredParam = new KernelParameterMetadata("required") { IsRequired = true };
var optionalParam = new KernelParameterMetadata("optional");
var attributes = new List<Attribute>();
var parameters = new List<KernelParameterMetadata> { requiredParam, optionalParam };
var function = KernelFunctionFactory.CreateFromMethod(() => true, ToolName, FunctionDescription, parameters);
var function = KernelFunctionFactory.CreateFromMethod(() => true, ToolName, FunctionDescription, attributes, parameters);

var toolModel = function.ToToolModel(PluginName);
var properties = toolModel.Function?.Parameters.Properties;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,15 @@ public abstract class KernelFunction
/// </summary>
/// <param name="name">A name of the function to use as its <see cref="KernelFunction.Name"/>.</param>
/// <param name="description">The description of the function to use as its <see cref="KernelFunction.Description"/>.</param>
/// <param name="attributes">The attributes on the function.</param>
/// <param name="parameters">The metadata describing the parameters to the function.</param>
/// <param name="returnParameter">The metadata describing the return parameter of the function.</param>
/// <param name="executionSettings">
/// The <see cref="PromptExecutionSettings"/> to use with the function. These will apply unless they've been
/// overridden by settings passed into the invocation of the function.
/// </param>
internal KernelFunction(string name, string description, IReadOnlyList<KernelParameterMetadata> parameters, KernelReturnParameterMetadata? returnParameter = null, Dictionary<string, PromptExecutionSettings>? executionSettings = null)
: this(name, null, description, parameters, returnParameter, executionSettings)
internal KernelFunction(string name, string description, IReadOnlyList<Attribute> attributes, IReadOnlyList<KernelParameterMetadata> parameters, KernelReturnParameterMetadata? returnParameter = null, Dictionary<string, PromptExecutionSettings>? executionSettings = null)
: this(name, null, description, attributes, parameters, returnParameter, executionSettings)
{
}

Expand All @@ -113,14 +114,15 @@ internal KernelFunction(string name, string description, IReadOnlyList<KernelPar
/// <param name="name">A name of the function to use as its <see cref="KernelFunction.Name"/>.</param>
/// <param name="pluginName">The name of the plugin this function instance has been added to.</param>
/// <param name="description">The description of the function to use as its <see cref="KernelFunction.Description"/>.</param>
/// <param name="attributes">The attributes on the function.</param>
/// <param name="parameters">The metadata describing the parameters to the function.</param>
/// <param name="returnParameter">The metadata describing the return parameter of the function.</param>
/// <param name="executionSettings">
/// The <see cref="PromptExecutionSettings"/> to use with the function. These will apply unless they've been
/// overridden by settings passed into the invocation of the function.
/// </param>
/// <param name="additionalMetadata">Properties/metadata associated with the function itself rather than its parameters and return type.</param>
internal KernelFunction(string name, string? pluginName, string description, IReadOnlyList<KernelParameterMetadata> parameters, KernelReturnParameterMetadata? returnParameter = null, Dictionary<string, PromptExecutionSettings>? executionSettings = null, ReadOnlyDictionary<string, object?>? additionalMetadata = null)
internal KernelFunction(string name, string? pluginName, string description, IReadOnlyList<Attribute> attributes, IReadOnlyList<KernelParameterMetadata> parameters, KernelReturnParameterMetadata? returnParameter = null, Dictionary<string, PromptExecutionSettings>? executionSettings = null, ReadOnlyDictionary<string, object?>? additionalMetadata = null)
{
Verify.NotNull(name);
Verify.ParametersUniqueness(parameters);
Expand All @@ -129,6 +131,7 @@ internal KernelFunction(string name, string? pluginName, string description, IRe
{
PluginName = pluginName,
Description = description,
Attributes = attributes,
Parameters = parameters,
ReturnParameter = returnParameter ?? KernelReturnParameterMetadata.Empty,
AdditionalProperties = additionalMetadata ?? KernelFunctionMetadata.s_emptyDictionary,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public sealed class KernelFunctionMetadata
private string _name = string.Empty;
/// <summary>The description of the function.</summary>
private string _description = string.Empty;
/// <summary>The function's attributes.</summary>
private IReadOnlyList<Attribute> _attributes = [];
/// <summary>The function's parameters.</summary>
private IReadOnlyList<KernelParameterMetadata> _parameters = [];
/// <summary>The function's return parameter.</summary>
Expand Down Expand Up @@ -46,6 +48,7 @@ public KernelFunctionMetadata(KernelFunctionMetadata metadata)
this.Name = metadata.Name;
this.PluginName = metadata.PluginName;
this.Description = metadata.Description;
this.Attributes = metadata.Attributes;
this.Parameters = metadata.Parameters;
this.ReturnParameter = metadata.ReturnParameter;
this.AdditionalProperties = metadata.AdditionalProperties;
Expand Down Expand Up @@ -74,6 +77,18 @@ public string Description
init => this._description = value ?? string.Empty;
}

/// <summary>Gets the attributes on the function.</summary>
/// <remarks>If the function has no attributes, the returned list will be empty.</remarks>
public IReadOnlyList<Attribute> Attributes
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it not make more sense to just expose the raw MethodInfo instance, rather than just the attributes?

{
get => this._attributes;
init
{
Verify.NotNull(value);
this._attributes = value;
}
}

/// <summary>Gets the metadata for the parameters to the function.</summary>
/// <remarks>If the function has no parameters, the returned list will be empty.</remarks>
public IReadOnlyList<KernelParameterMetadata> Parameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static class KernelFunctionFactory
/// <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="attributes">Optional function attributes. If null, it will default to those derived from the method represented by <paramref name="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>
Expand All @@ -31,10 +32,11 @@ public static KernelFunction CreateFromMethod(
Delegate method,
string? functionName = null,
string? description = null,
IEnumerable<Attribute>? attributes = null,
IEnumerable<KernelParameterMetadata>? parameters = null,
KernelReturnParameterMetadata? returnParameter = null,
ILoggerFactory? loggerFactory = null) =>
CreateFromMethod(method.Method, method.Target, functionName, description, parameters, returnParameter, loggerFactory);
CreateFromMethod(method.Method, method.Target, functionName, description, attributes, parameters, returnParameter, loggerFactory);

/// <summary>
/// Creates a <see cref="KernelFunction"/> instance for a method, specified via a delegate.
Expand All @@ -55,6 +57,7 @@ public static KernelFunction CreateFromMethod(
/// <param name="target">The target object for the <paramref name="method"/> if it represents an instance method. This should be null if and only if <paramref name="method"/> is a static method.</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="attributes">Optional function attributes. If null, it will default to those derived from the method represented by <paramref name="method"/>.</param>
/// <param name="parameters">Optional parameter descriptions. If null, it will default to ones 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>
Expand All @@ -64,10 +67,11 @@ public static KernelFunction CreateFromMethod(
object? target = null,
string? functionName = null,
string? description = null,
IEnumerable<Attribute>? attributes = null,
IEnumerable<KernelParameterMetadata>? parameters = null,
KernelReturnParameterMetadata? returnParameter = null,
ILoggerFactory? loggerFactory = null) =>
KernelFunctionFromMethod.Create(method, target, functionName, description, parameters, returnParameter, loggerFactory);
KernelFunctionFromMethod.Create(method, target, functionName, description, attributes, parameters, returnParameter, loggerFactory);

/// <summary>
/// Creates a <see cref="KernelFunction"/> instance for a method, specified via an <see cref="MethodInfo"/> instance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal sealed partial class KernelFunctionFromMethod : KernelFunction
/// <param name="target">The target object for the <paramref name="method"/> if it represents an instance method. This should be null if and only if <paramref name="method"/> is a static method.</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="attributes">Optional function attributes. If null, it will default to those derived from the method represented by <paramref name="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>
Expand All @@ -46,6 +47,7 @@ public static KernelFunction Create(
object? target = null,
string? functionName = null,
string? description = null,
IEnumerable<Attribute>? attributes = null,
IEnumerable<KernelParameterMetadata>? parameters = null,
KernelReturnParameterMetadata? returnParameter = null,
ILoggerFactory? loggerFactory = null)
Expand All @@ -57,6 +59,7 @@ public static KernelFunction Create(
{
FunctionName = functionName,
Description = description,
Attributes = attributes,
Parameters = parameters,
ReturnParameter = returnParameter,
LoggerFactory = loggerFactory
Expand Down Expand Up @@ -87,6 +90,7 @@ public static KernelFunction Create(
methodDetails.Function,
methodDetails.Name,
options?.Description ?? methodDetails.Description,
options?.Attributes?.ToList() ?? methodDetails.Attributes,
options?.Parameters?.ToList() ?? methodDetails.Parameters,
options?.ReturnParameter ?? methodDetails.ReturnParameter,
options?.AdditionalMetadata);
Expand Down Expand Up @@ -160,6 +164,7 @@ public override KernelFunction Clone(string pluginName)
this.Name,
pluginName,
this.Description,
this.Metadata.Attributes,
this.Metadata.Parameters,
this.Metadata.ReturnParameter,
this.Metadata.AdditionalProperties);
Expand All @@ -175,16 +180,17 @@ private delegate ValueTask<FunctionResult> ImplementationFunc(
private static readonly object[] s_cancellationTokenNoneArray = [CancellationToken.None];
private readonly ImplementationFunc _function;

private record struct MethodDetails(string Name, string Description, ImplementationFunc Function, List<KernelParameterMetadata> Parameters, KernelReturnParameterMetadata ReturnParameter);
private record struct MethodDetails(string Name, string Description, ImplementationFunc Function, List<Attribute> Attributes, List<KernelParameterMetadata> Parameters, KernelReturnParameterMetadata ReturnParameter);

private KernelFunctionFromMethod(
ImplementationFunc implementationFunc,
string functionName,
string description,
IReadOnlyList<Attribute> attributes,
IReadOnlyList<KernelParameterMetadata> parameters,
KernelReturnParameterMetadata returnParameter,
ReadOnlyDictionary<string, object?>? additionalMetadata = null) :
this(implementationFunc, functionName, null, description, parameters, returnParameter, additionalMetadata)
this(implementationFunc, functionName, null, description, attributes, parameters, returnParameter, additionalMetadata)
{
}

Expand All @@ -193,10 +199,11 @@ private KernelFunctionFromMethod(
string functionName,
string? pluginName,
string description,
IReadOnlyList<Attribute> attributes,
IReadOnlyList<KernelParameterMetadata> parameters,
KernelReturnParameterMetadata returnParameter,
ReadOnlyDictionary<string, object?>? additionalMetadata = null) :
base(functionName, pluginName, description, parameters, returnParameter, additionalMetadata: additionalMetadata)
base(functionName, pluginName, description, attributes, parameters, returnParameter, additionalMetadata: additionalMetadata)
{
Verify.ValidFunctionName(functionName);

Expand Down Expand Up @@ -281,6 +288,7 @@ ValueTask<FunctionResult> Function(Kernel kernel, KernelFunction function, Kerne
Function = Function,
Name = functionName!,
Description = method.GetCustomAttribute<DescriptionAttribute>(inherit: true)?.Description ?? "",
Attributes = [.. Attribute.GetCustomAttributes(method, inherit: true)],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only extracts attributes off the method. Possibly fine for a first pass but there's potentially value in also grabbing attributes off the plugin itself, parameters, etc.

Parameters = argParameterViews,
ReturnParameter = new KernelReturnParameterMetadata()
{
Expand Down Expand Up @@ -470,7 +478,7 @@ private static bool TryToDeserializeValue(object value, Type targetType, out obj
JsonDocument document => document.Deserialize(targetType),
JsonNode node => node.Deserialize(targetType),
JsonElement element => element.Deserialize(targetType),
// The JSON can be represented by other data types from various libraries. For example, JObject, JToken, and JValue from the Newtonsoft.Json library.
// The JSON can be represented by other data types from various libraries. For example, JObject, JToken, and JValue from the Newtonsoft.Json library.
// Since we don't take dependencies on these libraries and don't have access to the types here,
// the only way to deserialize those types is to convert them to a string first by calling the 'ToString' method.
// Attempting to use the 'JsonSerializer.Serialize' method, instead of calling the 'ToString' directly on those types, can lead to unpredictable outcomes.
Expand Down Expand Up @@ -739,7 +747,7 @@ private static void ThrowForInvalidSignatureIf([DoesNotReturnIf(true)] bool cond
{
if (input?.GetType() is Type type && converter.CanConvertFrom(type))
{
// This line performs string to type conversion
// This line performs string to type conversion
return converter.ConvertFrom(context: null, culture, input);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public sealed class KernelFunctionFromMethodOptions
/// </summary>
public string? Description { get; init; }

/// <summary>
/// Optional function attributes. If null, it will default to what is derived from the passed <see cref="Delegate"/> or <see cref="MethodInfo"/>.
/// </summary>
public IEnumerable<Attribute>? Attributes { get; init; }

/// <summary>
/// Optional parameter descriptions. If null, it will default to one derived from the passed <see cref="Delegate"/> or <see cref="MethodInfo"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ private KernelFunctionFromPrompt(
functionName ?? CreateRandomFunctionName(),
pluginName,
description ?? string.Empty,
attributes: [],
parameters,
returnParameter,
executionSettings)
Expand Down
Loading
Loading