From ee7e8823baf62d1a3b54554d18bc706050863d97 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 12 Feb 2025 17:02:26 +0000 Subject: [PATCH 1/9] Replace AIFunctionParemeterMetadata with MethodInfo --- .../Functions/AIFunctionMetadata.cs | 55 +++--------- .../Functions/AIFunctionParameterMetadata.cs | 63 -------------- .../AIFunctionReturnParameterMetadata.cs | 50 ----------- .../Utilities/AIJsonUtilities.Schema.cs | 30 ++++--- .../OpenAIModelMapper.ChatCompletion.cs | 17 ---- .../Functions/AIFunctionFactory.cs | 75 ++++++---------- .../AIFunctionFactoryCreateOptions.cs | 12 --- .../Contents/FunctionCallContentTests..cs | 9 -- .../Functions/AIFunctionMetadataTests.cs | 41 +-------- .../AIFunctionParameterMetadataTests.cs | 85 ------------------- .../AIFunctionReturnParameterMetadataTests.cs | 35 -------- .../Utilities/AIJsonUtilitiesTests.cs | 12 +-- .../PromptBasedFunctionCallingChatClient.cs | 14 +-- .../OpenAISerializationTests.cs | 6 +- .../Functions/AIFunctionFactoryTest.cs | 35 ++++---- 15 files changed, 92 insertions(+), 447 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionParameterMetadata.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionReturnParameterMetadata.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionParameterMetadataTests.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionReturnParameterMetadataTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs index d6e5279d589..bed1a45d4ef 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System.Reflection; using System.Text.Json; using Microsoft.Shared.Collections; using Microsoft.Shared.Diagnostics; @@ -25,18 +25,9 @@ public sealed class AIFunctionMetadata /// The JSON schema describing the function and its input parameters. private readonly JsonElement _schema = AIJsonUtilities.DefaultJsonSchema; - /// The function's parameters. - private readonly IReadOnlyList _parameters = []; - - /// The function's return parameter. - private readonly AIFunctionReturnParameterMetadata _returnParameter = AIFunctionReturnParameterMetadata.Empty; - /// Optional additional properties in addition to the named properties already available on this class. private readonly IReadOnlyDictionary _additionalProperties = EmptyReadOnlyDictionary.Instance; - /// indexed by name, lazily initialized. - private Dictionary? _parametersByName; - /// Initializes a new instance of the class for a function with the specified name. /// The name of the function. /// The was null. @@ -48,15 +39,13 @@ public AIFunctionMetadata(string name) /// Initializes a new instance of the class as a copy of another . /// The was null. /// - /// This creates a shallow clone of . The new instance's and - /// properties will return the same objects as in the original instance. + /// This creates a shallow clone of . /// public AIFunctionMetadata(AIFunctionMetadata metadata) { Name = Throw.IfNull(metadata).Name; Description = metadata.Description; - Parameters = metadata.Parameters; - ReturnParameter = metadata.ReturnParameter; + UnderlyingMethod = metadata.UnderlyingMethod; AdditionalProperties = metadata.AdditionalProperties; Schema = metadata.Schema; } @@ -76,33 +65,15 @@ public string Description init => _description = value ?? string.Empty; } - /// Gets the metadata for the parameters to the function. - /// If the function has no parameters, the returned list is empty. - public IReadOnlyList Parameters - { - get => _parameters; - init => _parameters = Throw.IfNull(value); - } - - /// Gets the for a parameter by its name. - /// The name of the parameter. - /// The corresponding , if found; otherwise, null. - public AIFunctionParameterMetadata? GetParameter(string name) - { - Dictionary? parametersByName = _parametersByName ??= _parameters.ToDictionary(p => p.Name); - - return parametersByName.TryGetValue(name, out AIFunctionParameterMetadata? parameter) ? - parameter : - null; - } - - /// Gets parameter metadata for the return parameter. - /// If the function has no return parameter, the value is a default instance of an . - public AIFunctionReturnParameterMetadata ReturnParameter - { - get => _returnParameter; - init => _returnParameter = Throw.IfNull(value); - } + /// + /// Gets the underlying .NET method that the function could be wrapping. + /// + /// + /// Used to provide additional metadata on the function and its signature. + /// Setting this property is optional and should have no impact on function invocation or its JSON schema, + /// which is how implementations interface with AI functions primarily. + /// + public MethodInfo? UnderlyingMethod { get; init; } /// Gets a JSON Schema describing the function and its input parameters. /// @@ -124,8 +95,6 @@ public AIFunctionReturnParameterMetadata ReturnParameter /// /// /// The metadata present in the schema document plays an important role in guiding AI function invocation. - /// Functions should incorporate as much detail as possible. The arity of the "properties" keyword should - /// also match the length of the list. /// /// /// When no schema is specified, consuming chat clients should assume the "{}" or "true" schema, indicating that any JSON input is admissible. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionParameterMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionParameterMetadata.cs deleted file mode 100644 index 372083fd799..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionParameterMetadata.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.AI; - -/// -/// Provides read-only metadata for an parameter. -/// -public sealed class AIFunctionParameterMetadata -{ - private readonly string _name; - - /// Initializes a new instance of the class for a parameter with the specified name. - /// The name of the parameter. - /// The was null. - /// The was empty or composed entirely of whitespace. - public AIFunctionParameterMetadata(string name) - { - _name = Throw.IfNullOrWhitespace(name); - } - - /// Initializes a new instance of the class as a copy of another . - /// The was null. - /// This constructor creates a shallow clone of . - public AIFunctionParameterMetadata(AIFunctionParameterMetadata metadata) - { - _ = Throw.IfNull(metadata); - _ = Throw.IfNullOrWhitespace(metadata.Name); - - _name = metadata.Name; - - Description = metadata.Description; - HasDefaultValue = metadata.HasDefaultValue; - DefaultValue = metadata.DefaultValue; - IsRequired = metadata.IsRequired; - ParameterType = metadata.ParameterType; - } - - /// Gets the name of the parameter. - public string Name - { - get => _name; - init => _name = Throw.IfNullOrWhitespace(value); - } - - /// Gets a description of the parameter, suitable for use in describing the purpose to a model. - public string? Description { get; init; } - - /// Gets a value indicating whether the parameter has a default value. - public bool HasDefaultValue { get; init; } - - /// Gets the default value of the parameter. - public object? DefaultValue { get; init; } - - /// Gets a value indicating whether the parameter is required. - public bool IsRequired { get; init; } - - /// Gets the .NET type of the parameter. - public Type? ParameterType { get; init; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionReturnParameterMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionReturnParameterMetadata.cs deleted file mode 100644 index 9ca5b3b6e49..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionReturnParameterMetadata.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Text.Json; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.AI; - -/// -/// Provides read-only metadata for an 's return parameter. -/// -public sealed class AIFunctionReturnParameterMetadata -{ - /// Gets an empty return parameter metadata instance. - public static AIFunctionReturnParameterMetadata Empty { get; } = new(); - - /// The JSON schema describing the function and its input parameters. - private readonly JsonElement _schema = AIJsonUtilities.DefaultJsonSchema; - - /// Initializes a new instance of the class. - public AIFunctionReturnParameterMetadata() - { - } - - /// Initializes a new instance of the class as a copy of another . - public AIFunctionReturnParameterMetadata(AIFunctionReturnParameterMetadata metadata) - { - Description = Throw.IfNull(metadata).Description; - ParameterType = metadata.ParameterType; - Schema = metadata.Schema; - } - - /// Gets a description of the return parameter, suitable for use in describing the purpose to a model. - public string? Description { get; init; } - - /// Gets the .NET type of the return parameter. - public Type? ParameterType { get; init; } - - /// Gets a JSON Schema describing the type of the return parameter. - public JsonElement Schema - { - get => _schema; - init - { - AIJsonUtilities.ValidateSchemaDocument(value); - _schema = value; - } - } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs index ee2e497638a..0443e40e224 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs @@ -2,11 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Nodes; @@ -46,39 +46,47 @@ public static partial class AIJsonUtilities private static readonly string[] _schemaKeywordsDisallowedByAIVendors = ["minLength", "maxLength", "pattern", "format"]; /// - /// Determines a JSON schema for the provided AI function parameter metadata. + /// Determines a JSON schema for the provided method signature. /// + /// The method from which to extract schema information. /// The title keyword used by the method schema. /// The description keyword used by the method schema. - /// The AI function parameter metadata. /// The options used to extract the schema from the specified type. /// The options controlling schema inference. /// A JSON schema document encoded as a . public static JsonElement CreateFunctionJsonSchema( + MethodBase methodInfo, string? title = null, string? description = null, - IReadOnlyList? parameters = null, JsonSerializerOptions? serializerOptions = null, AIJsonSchemaCreateOptions? inferenceOptions = null) { + _ = Throw.IfNull(methodInfo); serializerOptions ??= DefaultOptions; inferenceOptions ??= AIJsonSchemaCreateOptions.Default; + title ??= methodInfo.Name; + description ??= methodInfo.GetCustomAttribute()?.Description; JsonObject parameterSchemas = new(); JsonArray? requiredProperties = null; - foreach (AIFunctionParameterMetadata parameter in parameters ?? []) + foreach (ParameterInfo parameter in methodInfo.GetParameters()) { + if (string.IsNullOrWhiteSpace(parameter.Name)) + { + Throw.ArgumentException(nameof(parameter), "Parameter is missing a name."); + } + JsonNode parameterSchema = CreateJsonSchemaCore( - parameter.ParameterType, - parameter.Name, - parameter.Description, - parameter.HasDefaultValue, - parameter.DefaultValue, + type: parameter.ParameterType, + parameterName: parameter.Name, + description: parameter.GetCustomAttribute(inherit: true)?.Description, + hasDefaultValue: parameter.HasDefaultValue, + defaultValue: parameter.HasDefaultValue ? parameter.DefaultValue : null, serializerOptions, inferenceOptions); parameterSchemas.Add(parameter.Name, parameterSchema); - if (parameter.IsRequired) + if (!parameter.IsOptional) { (requiredProperties ??= []).Add((JsonNode)parameter.Name); } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs index 170f8cbe06e..3b7e35d5137 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs @@ -26,8 +26,6 @@ namespace Microsoft.Extensions.AI; internal static partial class OpenAIModelMappers { - internal static JsonElement DefaultParameterSchema { get; } = JsonDocument.Parse("{}").RootElement; - public static ChatCompletion ToOpenAIChatCompletion(ChatResponse response, JsonSerializerOptions options) { _ = Throw.IfNull(response); @@ -418,26 +416,11 @@ private static AITool FromOpenAIChatTool(ChatTool chatTool) } OpenAIChatToolJson openAiChatTool = JsonSerializer.Deserialize(chatTool.FunctionParameters.ToMemory().Span, OpenAIJsonContext.Default.OpenAIChatToolJson)!; - List parameters = new(openAiChatTool.Properties.Count); - foreach (KeyValuePair property in openAiChatTool.Properties) - { - parameters.Add(new(property.Key) - { - IsRequired = openAiChatTool.Required.Contains(property.Key), - }); - } - AIFunctionMetadata metadata = new(chatTool.FunctionName) { Description = chatTool.FunctionDescription, AdditionalProperties = additionalProperties, - Parameters = parameters, Schema = JsonSerializer.SerializeToElement(openAiChatTool, OpenAIJsonContext.Default.OpenAIChatToolJson), - ReturnParameter = new() - { - Description = "Return parameter", - Schema = DefaultParameterSchema, - } }; return new MetadataOnlyAIFunction(metadata); diff --git a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs index a3084882d75..b5fdf0540e3 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs @@ -242,46 +242,32 @@ static bool IsAsyncMethod(MethodInfo method) } } - // Build up a list of AIParameterMetadata for the parameters we expect to be populated - // from arguments. Some arguments are populated specially, not from arguments, and thus - // we don't want to advertise their metadata. - List? parameterMetadata = options.Parameters is not null ? null : []; - - // Get marshaling delegates for parameters and build up the parameter metadata. - var parameters = method.GetParameters(); + // Get marshaling delegates for parameters. + ParameterInfo[] parameters = method.GetParameters(); _parameterMarshallers = new Func, AIFunctionContext?, object?>[parameters.Length]; bool sawAIContextParameter = false; for (int i = 0; i < parameters.Length; i++) { - if (GetParameterMarshaller(options, parameters[i], ref sawAIContextParameter, out _parameterMarshallers[i]) is AIFunctionParameterMetadata parameterView) - { - parameterMetadata?.Add(parameterView); - } + _parameterMarshallers[i] = GetParameterMarshaller(options, parameters[i], ref sawAIContextParameter); } _needsAIFunctionContext = sawAIContextParameter; // Get the return type and a marshaling func for the return value. - Type returnType = GetReturnMarshaller(method, out _returnMarshaller); + _returnMarshaller = GetReturnMarshaller(method, out Type returnType); _returnTypeInfo = returnType != typeof(void) ? options.SerializerOptions.GetTypeInfo(returnType) : null; string? description = options.Description ?? method.GetCustomAttribute(inherit: true)?.Description; Metadata = new AIFunctionMetadata(functionName) { Description = description, - Parameters = options.Parameters ?? parameterMetadata!, - ReturnParameter = options.ReturnParameter ?? new() - { - ParameterType = returnType, - Description = method.ReturnParameter.GetCustomAttribute(inherit: true)?.Description, - Schema = AIJsonUtilities.CreateJsonSchema(returnType, serializerOptions: options.SerializerOptions, inferenceOptions: options.SchemaCreateOptions), - }, + UnderlyingMethod = method, AdditionalProperties = options.AdditionalProperties ?? EmptyReadOnlyDictionary.Instance, JsonSerializerOptions = options.SerializerOptions, Schema = AIJsonUtilities.CreateFunctionJsonSchema( + method, title: functionName, description: description, - parameters: options.Parameters ?? parameterMetadata, options.SerializerOptions, options.SchemaCreateOptions) }; @@ -321,7 +307,11 @@ static bool IsAsyncMethod(MethodInfo method) switch (_returnTypeInfo) { case null: - Debug.Assert(Metadata.ReturnParameter.ParameterType == typeof(void), "The return parameter is not void."); + Debug.Assert( + Metadata.UnderlyingMethod?.ReturnType == typeof(void) || + Metadata.UnderlyingMethod?.ReturnType == typeof(Task) || + Metadata.UnderlyingMethod?.ReturnType == typeof(ValueTask), "The return parameter should be void or non-generic task."); + return null; case { Kind: JsonTypeInfoKind.None }: @@ -342,11 +332,10 @@ static bool IsAsyncMethod(MethodInfo method) /// /// Gets a delegate for handling the marshaling of a parameter. /// - private static AIFunctionParameterMetadata? GetParameterMarshaller( + private static Func, AIFunctionContext?, object?> GetParameterMarshaller( AIFunctionFactoryCreateOptions options, ParameterInfo parameter, - ref bool sawAIFunctionContext, - out Func, AIFunctionContext?, object?> marshaller) + ref bool sawAIFunctionContext) { if (string.IsNullOrWhiteSpace(parameter.Name)) { @@ -363,12 +352,11 @@ static bool IsAsyncMethod(MethodInfo method) sawAIFunctionContext = true; - marshaller = static (_, ctx) => + return static (_, ctx) => { Debug.Assert(ctx is not null, "Expected a non-null context object."); return ctx; }; - return null; } // Resolve the contract used to marshal the value from JSON -- can throw if not supported or not found. @@ -376,7 +364,7 @@ static bool IsAsyncMethod(MethodInfo method) JsonTypeInfo typeInfo = options.SerializerOptions.GetTypeInfo(parameterType); // Create a marshaller that simply looks up the parameter by name in the arguments dictionary. - marshaller = (IReadOnlyDictionary arguments, AIFunctionContext? _) => + return (IReadOnlyDictionary arguments, AIFunctionContext? _) => { // If the parameter has an argument specified in the dictionary, return that argument. if (arguments.TryGetValue(parameter.Name, out object? value)) @@ -417,46 +405,36 @@ static bool IsAsyncMethod(MethodInfo method) // No default either. Leave it empty. return null; }; - - string? description = parameter.GetCustomAttribute(inherit: true)?.Description; - return new AIFunctionParameterMetadata(parameter.Name) - { - Description = description, - HasDefaultValue = parameter.HasDefaultValue, - DefaultValue = parameter.HasDefaultValue ? parameter.DefaultValue : null, - IsRequired = !parameter.IsOptional, - ParameterType = parameter.ParameterType, - }; } /// /// Gets a delegate for handling the result value of a method, converting it into the to return from the invocation. /// - private static Type GetReturnMarshaller(MethodInfo method, out Func> marshaller) + private static Func> GetReturnMarshaller(MethodInfo method, out Type returnType) { // Handle each known return type for the method - Type returnType = method.ReturnType; + returnType = method.ReturnType; // Task if (returnType == typeof(Task)) { - marshaller = async static result => + returnType = typeof(void); + return async static result => { await ((Task)ThrowIfNullResult(result)).ConfigureAwait(false); return null; }; - return typeof(void); } // ValueTask if (returnType == typeof(ValueTask)) { - marshaller = async static result => + returnType = typeof(void); + return async static result => { await ((ValueTask)ThrowIfNullResult(result)).ConfigureAwait(false); return null; }; - return typeof(void); } if (returnType.IsGenericType) @@ -465,12 +443,12 @@ private static Type GetReturnMarshaller(MethodInfo method, out Func)) { MethodInfo taskResultGetter = GetMethodFromGenericMethodDefinition(returnType, _taskGetResult); - marshaller = async result => + returnType = taskResultGetter.ReturnType; + return async result => { await ((Task)ThrowIfNullResult(result)).ConfigureAwait(false); return ReflectionInvoke(taskResultGetter, result, null); }; - return taskResultGetter.ReturnType; } // ValueTask @@ -478,19 +456,18 @@ private static Type GetReturnMarshaller(MethodInfo method, out Func + returnType = asTaskResultGetter.ReturnType; + return async result => { var task = (Task)ReflectionInvoke(valueTaskAsTask, ThrowIfNullResult(result), null)!; await task.ConfigureAwait(false); return ReflectionInvoke(asTaskResultGetter, task, null); }; - return asTaskResultGetter.ReturnType; } } // For everything else, just use the result as-is. - marshaller = result => new ValueTask(result); - return returnType; + return result => new ValueTask(result); // Throws an exception if a result is found to be null unexpectedly static object ThrowIfNullResult(object? result) => result ?? throw new InvalidOperationException("Function returned null unexpectedly."); diff --git a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryCreateOptions.cs b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryCreateOptions.cs index 1f33c6d4155..2043f09c2c6 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryCreateOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryCreateOptions.cs @@ -54,18 +54,6 @@ public AIJsonSchemaCreateOptions SchemaCreateOptions /// public string? Description { get; set; } - /// Gets or sets metadata for the parameters of the function. - /// - /// Metadata for the function's parameters. The default value is metadata derived from the passed or . - /// - public IReadOnlyList? Parameters { get; set; } - - /// Gets or sets metadata for function's return parameter. - /// - /// Metadata for the function's return parameter. The default value is metadata derived from the passed or . - /// - public AIFunctionReturnParameterMetadata? ReturnParameter { get; set; } - /// /// Gets or sets additional values to store on the resulting property. /// diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs index 747708602cd..4b1e2679f18 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs @@ -252,15 +252,6 @@ private sealed class NetTypelessAIFunction : AIFunction public override AIFunctionMetadata Metadata => new("NetTypeless") { Description = "AIFunction with parameters that lack .NET types", - Parameters = - [ - new AIFunctionParameterMetadata("a"), - new AIFunctionParameterMetadata("b"), - new AIFunctionParameterMetadata("c"), - new AIFunctionParameterMetadata("d"), - new AIFunctionParameterMetadata("e"), - new AIFunctionParameterMetadata("f"), - ] }; protected override Task InvokeCoreAsync(IEnumerable>? arguments, CancellationToken cancellationToken) => diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionMetadataTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionMetadataTests.cs index 08397144632..9e7f78ab3a2 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionMetadataTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionMetadataTests.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Text.Json; using Xunit; namespace Microsoft.Extensions.AI; @@ -24,12 +23,7 @@ public void Constructor_String_PropsDefaulted() AIFunctionMetadata f = new("name"); Assert.Equal("name", f.Name); Assert.Empty(f.Description); - Assert.Empty(f.Parameters); - - Assert.NotNull(f.ReturnParameter); - Assert.True(JsonElement.DeepEquals(f.ReturnParameter.Schema, JsonDocument.Parse("{}").RootElement)); - Assert.Null(f.ReturnParameter.ParameterType); - Assert.Null(f.ReturnParameter.Description); + Assert.Null(f.UnderlyingMethod); Assert.NotNull(f.AdditionalProperties); Assert.Empty(f.AdditionalProperties); @@ -42,16 +36,14 @@ public void Constructor_Copy_PropsPropagated() AIFunctionMetadata f1 = new("name") { Description = "description", - Parameters = [new AIFunctionParameterMetadata("param")], - ReturnParameter = new AIFunctionReturnParameterMetadata(), + UnderlyingMethod = typeof(AIFunctionMetadataTests).GetMethod(nameof(Constructor_Copy_PropsPropagated))!, AdditionalProperties = new Dictionary { { "key", "value" } }, }; AIFunctionMetadata f2 = new(f1); Assert.Equal(f1.Name, f2.Name); Assert.Equal(f1.Description, f2.Description); - Assert.Same(f1.Parameters, f2.Parameters); - Assert.Same(f1.ReturnParameter, f2.ReturnParameter); + Assert.Same(f1.UnderlyingMethod, f2.UnderlyingMethod); Assert.Same(f1.AdditionalProperties, f2.AdditionalProperties); } @@ -59,8 +51,6 @@ public void Constructor_Copy_PropsPropagated() public void Props_InvalidArg_Throws() { Assert.Throws("value", () => new AIFunctionMetadata("name") { Name = null! }); - Assert.Throws("value", () => new AIFunctionMetadata("name") { Parameters = null! }); - Assert.Throws("value", () => new AIFunctionMetadata("name") { ReturnParameter = null! }); Assert.Throws("value", () => new AIFunctionMetadata("name") { AdditionalProperties = null! }); } @@ -70,29 +60,4 @@ public void Description_NullNormalizedToEmpty() AIFunctionMetadata f = new("name") { Description = null }; Assert.Equal("", f.Description); } - - [Fact] - public void GetParameter_EmptyCollection_ReturnsNull() - { - Assert.Null(new AIFunctionMetadata("name").GetParameter("test")); - } - - [Fact] - public void GetParameter_ByName_ReturnsParameter() - { - AIFunctionMetadata f = new("name") - { - Parameters = - [ - new AIFunctionParameterMetadata("param0"), - new AIFunctionParameterMetadata("param1"), - new AIFunctionParameterMetadata("param2"), - ] - }; - - Assert.Same(f.Parameters[0], f.GetParameter("param0")); - Assert.Same(f.Parameters[1], f.GetParameter("param1")); - Assert.Same(f.Parameters[2], f.GetParameter("param2")); - Assert.Null(f.GetParameter("param3")); - } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionParameterMetadataTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionParameterMetadataTests.cs deleted file mode 100644 index 3eef60269a8..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionParameterMetadataTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Xunit; - -namespace Microsoft.Extensions.AI; - -public class AIFunctionParameterMetadataTests -{ - [Fact] - public void Constructor_InvalidArg_Throws() - { - Assert.Throws("name", () => new AIFunctionParameterMetadata((string)null!)); - Assert.Throws("name", () => new AIFunctionParameterMetadata(" ")); - Assert.Throws("metadata", () => new AIFunctionParameterMetadata((AIFunctionParameterMetadata)null!)); - } - - [Fact] - public void Constructor_String_PropsDefaulted() - { - AIFunctionParameterMetadata p = new("name"); - Assert.Equal("name", p.Name); - Assert.Null(p.Description); - Assert.Null(p.DefaultValue); - Assert.False(p.IsRequired); - Assert.Null(p.ParameterType); - } - - [Fact] - public void Constructor_Copy_PropsPropagated() - { - AIFunctionParameterMetadata p1 = new("name") - { - Description = "description", - HasDefaultValue = true, - DefaultValue = 42, - IsRequired = true, - ParameterType = typeof(int), - }; - - AIFunctionParameterMetadata p2 = new(p1); - - Assert.Equal(p1.Name, p2.Name); - Assert.Equal(p1.Description, p2.Description); - Assert.Equal(p1.DefaultValue, p2.DefaultValue); - Assert.Equal(p1.IsRequired, p2.IsRequired); - Assert.Equal(p1.ParameterType, p2.ParameterType); - } - - [Fact] - public void Constructor_Copy_PropsPropagatedAndOverwritten() - { - AIFunctionParameterMetadata p1 = new("name") - { - Description = "description", - HasDefaultValue = true, - DefaultValue = 42, - IsRequired = true, - ParameterType = typeof(int), - }; - - AIFunctionParameterMetadata p2 = new(p1) - { - Description = "description2", - HasDefaultValue = true, - DefaultValue = 43, - IsRequired = false, - ParameterType = typeof(long), - }; - - Assert.Equal("description2", p2.Description); - Assert.True(p2.HasDefaultValue); - Assert.Equal(43, p2.DefaultValue); - Assert.False(p2.IsRequired); - Assert.Equal(typeof(long), p2.ParameterType); - } - - [Fact] - public void Props_InvalidArg_Throws() - { - Assert.Throws("value", () => new AIFunctionMetadata("name") { Name = null! }); - Assert.Throws("value", () => new AIFunctionMetadata("name") { Name = "\r\n\t " }); - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionReturnParameterMetadataTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionReturnParameterMetadataTests.cs deleted file mode 100644 index d4721501093..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionReturnParameterMetadataTests.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text.Json; -using Xunit; - -namespace Microsoft.Extensions.AI; - -public class AIFunctionReturnParameterMetadataTests -{ - [Fact] - public void Constructor_PropsDefaulted() - { - AIFunctionReturnParameterMetadata p = new(); - Assert.Null(p.Description); - Assert.Null(p.ParameterType); - Assert.True(JsonElement.DeepEquals(p.Schema, JsonDocument.Parse("{}").RootElement)); - } - - [Fact] - public void Constructor_Copy_PropsPropagated() - { - AIFunctionReturnParameterMetadata p1 = new() - { - Description = "description", - ParameterType = typeof(int), - Schema = JsonDocument.Parse("""{"type":"integer"}""").RootElement, - }; - - AIFunctionReturnParameterMetadata p2 = new(p1); - Assert.Equal(p1.Description, p2.Description); - Assert.Equal(p1.ParameterType, p2.ParameterType); - Assert.Equal(p1.Schema, p2.Schema); - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs index 4d654e042ff..3a769af788d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel; using System.Linq; +using System.Reflection; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; @@ -230,10 +231,9 @@ public static void CreateFunctionJsonSchema_ReturnsExpectedValue() JsonSerializerOptions options = new(JsonSerializerOptions.Default); AIFunction func = AIFunctionFactory.Create((int x, int y) => x + y, serializerOptions: options); - AIFunctionMetadata metadata = func.Metadata; - AIFunctionParameterMetadata param = metadata.Parameters[0]; + Assert.NotNull(func.Metadata.UnderlyingMethod); - JsonElement resolvedSchema = AIJsonUtilities.CreateFunctionJsonSchema(title: func.Metadata.Name, description: func.Metadata.Description, parameters: func.Metadata.Parameters); + JsonElement resolvedSchema = AIJsonUtilities.CreateFunctionJsonSchema(func.Metadata.UnderlyingMethod, title: func.Metadata.Name); Assert.True(JsonElement.DeepEquals(resolvedSchema, func.Metadata.Schema)); } @@ -245,12 +245,14 @@ public static void CreateFunctionJsonSchema_TreatsIntegralTypesAsInteger_EvenWit AIFunctionMetadata metadata = func.Metadata; JsonElement schemaParameters = func.Metadata.Schema.GetProperty("properties"); - Assert.Equal(metadata.Parameters.Count, schemaParameters.GetPropertyCount()); + Assert.NotNull(metadata.UnderlyingMethod); + ParameterInfo[] parameters = metadata.UnderlyingMethod.GetParameters(); + Assert.Equal(parameters.Length, schemaParameters.GetPropertyCount()); int i = 0; foreach (JsonProperty property in schemaParameters.EnumerateObject()) { - string numericType = Type.GetTypeCode(metadata.Parameters[i].ParameterType) is TypeCode.Double or TypeCode.Single or TypeCode.Decimal + string numericType = Type.GetTypeCode(parameters[i].ParameterType) is TypeCode.Double or TypeCode.Single or TypeCode.Decimal ? "number" : "integer"; diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/PromptBasedFunctionCallingChatClient.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/PromptBasedFunctionCallingChatClient.cs index b442fa74b40..7e86f8da8c5 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/PromptBasedFunctionCallingChatClient.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/PromptBasedFunctionCallingChatClient.cs @@ -4,7 +4,9 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Linq; +using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; @@ -181,14 +183,14 @@ answer the user's question without repeating the same tool call. { Name = tool.Metadata.Name, Description = tool.Metadata.Description, - Arguments = tool.Metadata.Parameters.ToDictionary( - p => p.Name, + Arguments = tool.Metadata.UnderlyingMethod!.GetParameters().ToDictionary( + p => p.Name!, p => new ToolParameterDescriptor { - Type = p.ParameterType?.Name, - Description = p.Description, - Enum = p.ParameterType?.IsEnum == true ? Enum.GetNames(p.ParameterType) : null, - Required = p.IsRequired, + Type = p.Name!, + Description = p.GetCustomAttribute()?.Description, + Enum = p.ParameterType.IsEnum ? Enum.GetNames(p.ParameterType) : null, + Required = !p.IsOptional, }), }; diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISerializationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISerializationTests.cs index 205229f0cfd..66dd1212abc 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISerializationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISerializationTests.cs @@ -373,12 +373,8 @@ public static async Task RequestDeserialization_ToolCall() Assert.Equal("Gets the age of the specified person.", function.Metadata.Description); Assert.Equal("GetPersonAge", function.Metadata.Name); Assert.Equal("Strict", Assert.Single(function.Metadata.AdditionalProperties).Key); - Assert.Equal("Return parameter", function.Metadata.ReturnParameter.Description); - Assert.Equal("{}", Assert.IsType(function.Metadata.ReturnParameter.Schema).GetRawText()); - AIFunctionParameterMetadata parameter = Assert.Single(function.Metadata.Parameters); - Assert.Equal("personName", parameter.Name); - Assert.True(parameter.IsRequired); + Assert.Null(function.Metadata.UnderlyingMethod); JsonObject parametersSchema = Assert.IsType(JsonNode.Parse(function.Metadata.Schema.GetProperty("properties").GetRawText())); var parameterSchema = Assert.IsType(Assert.Single(parametersSchema.Select(kvp => kvp.Value))); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index 207a4705751..b0016e0adc1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -132,54 +133,50 @@ public void Metadata_DerivedFromLambda() { AIFunction func; - func = AIFunctionFactory.Create(() => "test"); + Func dotnetFunc = () => "test"; + func = AIFunctionFactory.Create(dotnetFunc); Assert.Contains("Metadata_DerivedFromLambda", func.Metadata.Name); Assert.Empty(func.Metadata.Description); - Assert.Empty(func.Metadata.Parameters); - Assert.Equal(typeof(string), func.Metadata.ReturnParameter.ParameterType); + Assert.Same(dotnetFunc.Method, func.Metadata.UnderlyingMethod); - func = AIFunctionFactory.Create((string a) => a + " " + a); + Func dotnetFunc2 = (string a) => a + " " + a; + func = AIFunctionFactory.Create(dotnetFunc2); Assert.Contains("Metadata_DerivedFromLambda", func.Metadata.Name); Assert.Empty(func.Metadata.Description); - Assert.Single(func.Metadata.Parameters); + Assert.Same(dotnetFunc2.Method, func.Metadata.UnderlyingMethod); - func = AIFunctionFactory.Create( - [Description("This is a test function")] ([Description("This is A")] string a, [Description("This is B")] string b) => b + " " + a); + Func dotnetFunc3 = [Description("This is a test function")] ([Description("This is A")] string a, [Description("This is B")] string b) => b + " " + a; + func = AIFunctionFactory.Create(dotnetFunc3); Assert.Contains("Metadata_DerivedFromLambda", func.Metadata.Name); Assert.Equal("This is a test function", func.Metadata.Description); - Assert.Collection(func.Metadata.Parameters, - p => Assert.Equal("This is A", p.Description), - p => Assert.Equal("This is B", p.Description)); + Assert.Same(dotnetFunc3.Method, func.Metadata.UnderlyingMethod); + Assert.Collection(func.Metadata.UnderlyingMethod!.GetParameters(), + p => Assert.Equal("This is A", p.GetCustomAttribute()?.Description), + p => Assert.Equal("This is B", p.GetCustomAttribute()?.Description)); } [Fact] public void AIFunctionFactoryCreateOptions_ValuesPropagateToAIFunction() { - IReadOnlyList parameterMetadata = [new AIFunctionParameterMetadata("a")]; - AIFunctionReturnParameterMetadata returnParameterMetadata = new() { ParameterType = typeof(string) }; IReadOnlyDictionary metadata = new Dictionary { ["a"] = "b" }; var options = new AIFunctionFactoryCreateOptions { Name = "test name", Description = "test description", - Parameters = parameterMetadata, - ReturnParameter = returnParameterMetadata, AdditionalProperties = metadata, }; Assert.Equal("test name", options.Name); Assert.Equal("test description", options.Description); - Assert.Same(parameterMetadata, options.Parameters); - Assert.Same(returnParameterMetadata, options.ReturnParameter); Assert.Same(metadata, options.AdditionalProperties); - AIFunction func = AIFunctionFactory.Create(() => { }, options); + Action dotnetFunc = () => { }; + AIFunction func = AIFunctionFactory.Create(dotnetFunc, options); Assert.Equal("test name", func.Metadata.Name); Assert.Equal("test description", func.Metadata.Description); - Assert.Equal(parameterMetadata, func.Metadata.Parameters); - Assert.Equal(returnParameterMetadata, func.Metadata.ReturnParameter); + Assert.Same(dotnetFunc.Method, func.Metadata.UnderlyingMethod); Assert.Equal(metadata, func.Metadata.AdditionalProperties); } From 54fdfa1498e3a6303bb2029e0a106a0b898eb406 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 12 Feb 2025 19:54:16 +0000 Subject: [PATCH 2/9] Update src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs Co-authored-by: Stephen Toub --- .../Functions/AIFunctionMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs index bed1a45d4ef..a776c4a120f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs @@ -69,7 +69,7 @@ public string Description /// Gets the underlying .NET method that the function could be wrapping. /// /// - /// Used to provide additional metadata on the function and its signature. + /// This property provides additional metadata on the function and its signature. /// Setting this property is optional and should have no impact on function invocation or its JSON schema, /// which is how implementations interface with AI functions primarily. /// From ec20fb58244135bdf7c3df12e2e87d29c9a599d5 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 12 Feb 2025 19:54:27 +0000 Subject: [PATCH 3/9] Update src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs Co-authored-by: Stephen Toub --- .../Functions/AIFunctionMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs index a776c4a120f..8cf9de65651 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs @@ -66,7 +66,7 @@ public string Description } /// - /// Gets the underlying .NET method that the function could be wrapping. + /// Gets a for the underlying .NET method this represents. /// /// /// This property provides additional metadata on the function and its signature. From 2ca8c7b4bb6988ea9ee1902fb3a3f46d7d2af61e Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 12 Feb 2025 20:29:05 +0000 Subject: [PATCH 4/9] Address feedback, --- .../PromptBasedFunctionCallingChatClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/PromptBasedFunctionCallingChatClient.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/PromptBasedFunctionCallingChatClient.cs index 7e86f8da8c5..2a32f39c951 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/PromptBasedFunctionCallingChatClient.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/PromptBasedFunctionCallingChatClient.cs @@ -183,7 +183,7 @@ answer the user's question without repeating the same tool call. { Name = tool.Metadata.Name, Description = tool.Metadata.Description, - Arguments = tool.Metadata.UnderlyingMethod!.GetParameters().ToDictionary( + Arguments = tool.Metadata.UnderlyingMethod?.GetParameters().ToDictionary( p => p.Name!, p => new ToolParameterDescriptor { @@ -191,7 +191,7 @@ answer the user's question without repeating the same tool call. Description = p.GetCustomAttribute()?.Description, Enum = p.ParameterType.IsEnum ? Enum.GetNames(p.ParameterType) : null, Required = !p.IsOptional, - }), + }) ?? [], }; private sealed class ToolDescriptor From 3eaa45280d8c0d5ea56382b68389a4bd6952040c Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 13 Feb 2025 12:39:41 -0500 Subject: [PATCH 5/9] Update src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs Co-authored-by: Steve Sanderson --- .../Utilities/AIJsonUtilities.Schema.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs index 0443e40e224..1706b2c00c8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs @@ -46,7 +46,7 @@ public static partial class AIJsonUtilities private static readonly string[] _schemaKeywordsDisallowedByAIVendors = ["minLength", "maxLength", "pattern", "format"]; /// - /// Determines a JSON schema for the provided method signature. + /// Determines a JSON schema for the provided method. /// /// The method from which to extract schema information. /// The title keyword used by the method schema. From e8c868855cfb4464841e21ea1dd4e669c8a4c1e7 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 13 Feb 2025 18:44:21 +0000 Subject: [PATCH 6/9] Flatten AIFunctionMetadata properties into AIFunction. --- .../Functions/AIFunction.cs | 88 ++++++++++++- .../Functions/AIFunctionMetadata.cs | 122 ------------------ .../AzureAIInferenceChatClient.cs | 6 +- .../OllamaChatClient.cs | 6 +- .../OpenAIAssistantClient.cs | 6 +- .../OpenAIModelMapper.ChatCompletion.cs | 18 ++- .../OpenAIRealtimeExtensions.cs | 8 +- .../FunctionInvokingChatClient.cs | 16 +-- .../Functions/AIFunctionFactory.cs | 59 +++------ .../AIFunctionFactoryCreateOptions.cs | 10 +- .../Contents/FunctionCallContentTests..cs | 7 +- .../Functions/AIFunctionMetadataTests.cs | 63 --------- .../Functions/AIFunctionTests.cs | 2 +- .../Utilities/AIJsonUtilitiesTests.cs | 13 +- .../ChatClientIntegrationTests.cs | 2 +- .../PromptBasedFunctionCallingChatClient.cs | 6 +- .../OpenAISerializationTests.cs | 10 +- .../FunctionInvokingChatClientTests.cs | 4 +- .../Functions/AIFunctionFactoryTest.cs | 30 ++--- 19 files changed, 165 insertions(+), 311 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionMetadataTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs index a4b5ecb5378..b45a829e79b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs @@ -1,11 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Collections; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -13,8 +18,80 @@ namespace Microsoft.Extensions.AI; [DebuggerDisplay("{DebuggerDisplay,nq}")] public abstract class AIFunction : AITool { - /// Gets metadata describing the function. - public abstract AIFunctionMetadata Metadata { get; } + /// The description of the function. + private readonly string _description = string.Empty; + + /// The JSON schema describing the function and its input parameters. + private readonly JsonElement _jsonSchema = AIJsonUtilities.DefaultJsonSchema; + + /// Optional additional properties in addition to the named properties already available on this class. + private readonly IReadOnlyDictionary _additionalProperties = EmptyReadOnlyDictionary.Instance; + + /// Gets the name of the function. + public abstract string Name { get; } + + /// Gets a description of the function, suitable for use in describing the purpose to a model. + [AllowNull] + public virtual string Description + { + get => _description; + init => _description = value ?? string.Empty; + } + + /// + /// Gets a for the underlying .NET method this represents. + /// + /// + /// This property provides additional metadata on the function and its signature. + /// Setting this property is optional and should have no impact on function invocation or its JSON schema, + /// which is how implementations interface with AI functions primarily. + /// + public virtual MethodInfo? UnderlyingMethod { get; init; } + + /// Gets a JSON Schema describing the function and its input parameters. + /// + /// + /// When specified, declares a self-contained JSON schema document that describes the function and its input parameters. + /// A simple example of a JSON schema for a function that adds two numbers together is shown below: + /// + /// + /// { + /// "title" : "addNumbers", + /// "description": "A simple function that adds two numbers together.", + /// "type": "object", + /// "properties": { + /// "a" : { "type": "number" }, + /// "b" : { "type": "number", "default": 1 } + /// }, + /// "required" : ["a"] + /// } + /// + /// + /// The metadata present in the schema document plays an important role in guiding AI function invocation. + /// + /// + /// When no schema is specified, consuming chat clients should assume the "{}" or "true" schema, indicating that any JSON input is admissible. + /// + /// + public virtual JsonElement JsonSchema + { + get => _jsonSchema; + init + { + AIJsonUtilities.ValidateSchemaDocument(value); + _jsonSchema = value; + } + } + + /// Gets any additional properties associated with the function. + public virtual IReadOnlyDictionary AdditionalProperties + { + get => _additionalProperties; + init => _additionalProperties = Throw.IfNull(value); + } + + /// Gets a that can be used to marshal function parameters. + public JsonSerializerOptions? JsonSerializerOptions { get; init; } /// Invokes the and returns its result. /// The arguments to pass to the function's invocation. @@ -30,7 +107,7 @@ public abstract class AIFunction : AITool } /// - public override string ToString() => Metadata.Name; + public override string ToString() => Name; /// Invokes the and returns its result. /// The arguments to pass to the function's invocation. @@ -42,8 +119,5 @@ public abstract class AIFunction : AITool /// Gets the string to display in the debugger for this instance. [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string DebuggerDisplay => - string.IsNullOrWhiteSpace(Metadata.Description) ? - Metadata.Name : - $"{Metadata.Name} ({Metadata.Description})"; + private string DebuggerDisplay => string.IsNullOrWhiteSpace(Description) ? Name : $"{Name} ({Description})"; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs deleted file mode 100644 index 8cf9de65651..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs +++ /dev/null @@ -1,122 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Text.Json; -using Microsoft.Shared.Collections; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.AI; - -/// -/// Provides read-only metadata for an . -/// -public sealed class AIFunctionMetadata -{ - /// The name of the function. - private readonly string _name = string.Empty; - - /// The description of the function. - private readonly string _description = string.Empty; - - /// The JSON schema describing the function and its input parameters. - private readonly JsonElement _schema = AIJsonUtilities.DefaultJsonSchema; - - /// Optional additional properties in addition to the named properties already available on this class. - private readonly IReadOnlyDictionary _additionalProperties = EmptyReadOnlyDictionary.Instance; - - /// Initializes a new instance of the class for a function with the specified name. - /// The name of the function. - /// The was null. - public AIFunctionMetadata(string name) - { - _name = Throw.IfNullOrWhitespace(name); - } - - /// Initializes a new instance of the class as a copy of another . - /// The was null. - /// - /// This creates a shallow clone of . - /// - public AIFunctionMetadata(AIFunctionMetadata metadata) - { - Name = Throw.IfNull(metadata).Name; - Description = metadata.Description; - UnderlyingMethod = metadata.UnderlyingMethod; - AdditionalProperties = metadata.AdditionalProperties; - Schema = metadata.Schema; - } - - /// Gets the name of the function. - public string Name - { - get => _name; - init => _name = Throw.IfNullOrWhitespace(value); - } - - /// Gets a description of the function, suitable for use in describing the purpose to a model. - [AllowNull] - public string Description - { - get => _description; - init => _description = value ?? string.Empty; - } - - /// - /// Gets a for the underlying .NET method this represents. - /// - /// - /// This property provides additional metadata on the function and its signature. - /// Setting this property is optional and should have no impact on function invocation or its JSON schema, - /// which is how implementations interface with AI functions primarily. - /// - public MethodInfo? UnderlyingMethod { get; init; } - - /// Gets a JSON Schema describing the function and its input parameters. - /// - /// - /// When specified, declares a self-contained JSON schema document that describes the function and its input parameters. - /// A simple example of a JSON schema for a function that adds two numbers together is shown below: - /// - /// - /// { - /// "title" : "addNumbers", - /// "description": "A simple function that adds two numbers together.", - /// "type": "object", - /// "properties": { - /// "a" : { "type": "number" }, - /// "b" : { "type": "number", "default": 1 } - /// }, - /// "required" : ["a"] - /// } - /// - /// - /// The metadata present in the schema document plays an important role in guiding AI function invocation. - /// - /// - /// When no schema is specified, consuming chat clients should assume the "{}" or "true" schema, indicating that any JSON input is admissible. - /// - /// - public JsonElement Schema - { - get => _schema; - init - { - AIJsonUtilities.ValidateSchemaDocument(value); - _schema = value; - } - } - - /// Gets any additional properties associated with the function. - public IReadOnlyDictionary AdditionalProperties - { - get => _additionalProperties; - init => _additionalProperties = Throw.IfNull(value); - } - - /// Gets a that can be used to marshal function parameters. - public JsonSerializerOptions? JsonSerializerOptions { get; init; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs index 27e6298e057..bb23e5da7e5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs @@ -380,11 +380,11 @@ private ChatCompletionsOptions ToAzureAIOptions(IList chatContents, private static ChatCompletionsToolDefinition ToAzureAIChatTool(AIFunction aiFunction) { // Map to an intermediate model so that redundant properties are skipped. - var tool = JsonSerializer.Deserialize(aiFunction.Metadata.Schema, JsonContext.Default.AzureAIChatToolJson)!; + var tool = JsonSerializer.Deserialize(aiFunction.JsonSchema, JsonContext.Default.AzureAIChatToolJson)!; var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, JsonContext.Default.AzureAIChatToolJson)); - return new(new FunctionDefinition(aiFunction.Metadata.Name) + return new(new FunctionDefinition(aiFunction.Name) { - Description = aiFunction.Metadata.Description, + Description = aiFunction.Description, Parameters = functionParameters, }); } diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs index efeff58d592..727d00f05c8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs @@ -476,9 +476,9 @@ private static OllamaTool ToOllamaTool(AIFunction function) Type = "function", Function = new OllamaFunctionTool { - Name = function.Metadata.Name, - Description = function.Metadata.Description, - Parameters = JsonSerializer.Deserialize(function.Metadata.Schema, JsonContext.Default.OllamaFunctionToolParameters)!, + Name = function.Name, + Description = function.Description, + Parameters = JsonSerializer.Deserialize(function.JsonSchema, JsonContext.Default.OllamaFunctionToolParameters)!, } }; } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantClient.cs index 110ea0bf7fe..96bd316d746 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantClient.cs @@ -215,16 +215,16 @@ private static (RunCreationOptions RunOptions, List? Tool if (tool is AIFunction aiFunction) { bool? strict = - aiFunction.Metadata.AdditionalProperties.TryGetValue("Strict", out object? strictObj) && + aiFunction.AdditionalProperties.TryGetValue("Strict", out object? strictObj) && strictObj is bool strictValue ? strictValue : null; var functionParameters = BinaryData.FromBytes( JsonSerializer.SerializeToUtf8Bytes( - JsonSerializer.Deserialize(aiFunction.Metadata.Schema, OpenAIJsonContext.Default.OpenAIChatToolJson)!, + JsonSerializer.Deserialize(aiFunction.JsonSchema, OpenAIJsonContext.Default.OpenAIChatToolJson)!, OpenAIJsonContext.Default.OpenAIChatToolJson)); - runOptions.ToolsOverride.Add(ToolDefinition.CreateFunction(aiFunction.Metadata.Name, aiFunction.Metadata.Description, functionParameters, strict)); + runOptions.ToolsOverride.Add(ToolDefinition.CreateFunction(aiFunction.Name, aiFunction.Description, functionParameters, strict)); } } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs index 3b7e35d5137..f18c76a137e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs @@ -416,35 +416,33 @@ private static AITool FromOpenAIChatTool(ChatTool chatTool) } OpenAIChatToolJson openAiChatTool = JsonSerializer.Deserialize(chatTool.FunctionParameters.ToMemory().Span, OpenAIJsonContext.Default.OpenAIChatToolJson)!; - AIFunctionMetadata metadata = new(chatTool.FunctionName) + return new MetadataOnlyAIFunction(chatTool.FunctionName) { Description = chatTool.FunctionDescription, AdditionalProperties = additionalProperties, - Schema = JsonSerializer.SerializeToElement(openAiChatTool, OpenAIJsonContext.Default.OpenAIChatToolJson), + JsonSchema = JsonSerializer.SerializeToElement(openAiChatTool, OpenAIJsonContext.Default.OpenAIChatToolJson), }; - - return new MetadataOnlyAIFunction(metadata); } - private sealed class MetadataOnlyAIFunction(AIFunctionMetadata metadata) : AIFunction + private sealed class MetadataOnlyAIFunction(string name) : AIFunction { - public override AIFunctionMetadata Metadata => metadata; + public override string Name => name; protected override Task InvokeCoreAsync(IEnumerable> arguments, CancellationToken cancellationToken) => - throw new InvalidOperationException($"The AI function '{metadata.Name}' does not support being invoked."); + throw new InvalidOperationException($"The AI function '{Name}' does not support being invoked."); } /// Converts an Extensions function to an OpenAI chat tool. private static ChatTool ToOpenAIChatTool(AIFunction aiFunction) { bool? strict = - aiFunction.Metadata.AdditionalProperties.TryGetValue("Strict", out object? strictObj) && + aiFunction.AdditionalProperties.TryGetValue("Strict", out object? strictObj) && strictObj is bool strictValue ? strictValue : null; // Map to an intermediate model so that redundant properties are skipped. - var tool = JsonSerializer.Deserialize(aiFunction.Metadata.Schema, OpenAIJsonContext.Default.OpenAIChatToolJson)!; + var tool = JsonSerializer.Deserialize(aiFunction.JsonSchema, OpenAIJsonContext.Default.OpenAIChatToolJson)!; var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, OpenAIJsonContext.Default.OpenAIChatToolJson)); - return ChatTool.CreateFunctionTool(aiFunction.Metadata.Name, aiFunction.Metadata.Description, functionParameters, strict); + return ChatTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, functionParameters, strict); } private static UsageDetails FromOpenAIUsage(ChatTokenUsage tokenUsage) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeExtensions.cs index db12baf962d..8a652a71766 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeExtensions.cs @@ -27,12 +27,12 @@ public static ConversationFunctionTool ToConversationFunctionTool(this AIFunctio { _ = Throw.IfNull(aiFunction); - ConversationFunctionToolParametersSchema functionToolSchema = JsonSerializer.Deserialize(aiFunction.Metadata.Schema, OpenAIJsonContext.Default.ConversationFunctionToolParametersSchema)!; + ConversationFunctionToolParametersSchema functionToolSchema = JsonSerializer.Deserialize(aiFunction.JsonSchema, OpenAIJsonContext.Default.ConversationFunctionToolParametersSchema)!; BinaryData functionParameters = new(JsonSerializer.SerializeToUtf8Bytes(functionToolSchema, OpenAIJsonContext.Default.ConversationFunctionToolParametersSchema)); return new ConversationFunctionTool { - Name = aiFunction.Metadata.Name, - Description = aiFunction.Metadata.Description, + Name = aiFunction.Name, + Description = aiFunction.Description, Parameters = functionParameters }; } @@ -92,7 +92,7 @@ public static async Task HandleToolCallsAsync( CancellationToken cancellationToken = default) { if (!string.IsNullOrEmpty(update.FunctionName) - && tools.FirstOrDefault(t => t.Metadata.Name == update.FunctionName) is AIFunction aiFunction) + && tools.FirstOrDefault(t => t.Name == update.FunctionName) is AIFunction aiFunction) { var jsonOptions = jsonSerializerOptions ?? AIJsonUtilities.DefaultOptions; diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 83522fca6c8..2420c401ceb 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -555,7 +555,7 @@ private async Task ProcessFunctionCallAsync( int iteration, int functionCallIndex, int totalFunctionCount, CancellationToken cancellationToken) { // Look up the AIFunction for the function call. If the requested function isn't available, send back an error. - AIFunction? function = options.Tools!.OfType().FirstOrDefault(t => t.Metadata.Name == functionCallContent.Name); + AIFunction? function = options.Tools!.OfType().FirstOrDefault(t => t.Name == functionCallContent.Name); if (function is null) { return new(ContinueMode.Continue, FunctionStatus.NotFound, functionCallContent, result: null, exception: null); @@ -661,7 +661,7 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul { _ = Throw.IfNull(context); - using Activity? activity = _activitySource?.StartActivity(context.Function.Metadata.Name); + using Activity? activity = _activitySource?.StartActivity(context.Function.Name); long startingTimestamp = 0; if (_logger.IsEnabled(LogLevel.Debug)) @@ -669,11 +669,11 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul startingTimestamp = Stopwatch.GetTimestamp(); if (_logger.IsEnabled(LogLevel.Trace)) { - LogInvokingSensitive(context.Function.Metadata.Name, LoggingHelpers.AsJson(context.CallContent.Arguments, context.Function.Metadata.JsonSerializerOptions)); + LogInvokingSensitive(context.Function.Name, LoggingHelpers.AsJson(context.CallContent.Arguments, context.Function.JsonSerializerOptions)); } else { - LogInvoking(context.Function.Metadata.Name); + LogInvoking(context.Function.Name); } } @@ -693,11 +693,11 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul if (e is OperationCanceledException) { - LogInvocationCanceled(context.Function.Metadata.Name); + LogInvocationCanceled(context.Function.Name); } else { - LogInvocationFailed(context.Function.Metadata.Name, e); + LogInvocationFailed(context.Function.Name, e); } throw; @@ -710,11 +710,11 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul if (result is not null && _logger.IsEnabled(LogLevel.Trace)) { - LogInvocationCompletedSensitive(context.Function.Metadata.Name, elapsed, LoggingHelpers.AsJson(result, context.Function.Metadata.JsonSerializerOptions)); + LogInvocationCompletedSensitive(context.Function.Name, elapsed, LoggingHelpers.AsJson(result, context.Function.JsonSerializerOptions)); } else { - LogInvocationCompleted(context.Function.Metadata.Name, elapsed); + LogInvocationCompleted(context.Function.Name, elapsed); } } } diff --git a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs index b5fdf0540e3..c7aef70c88b 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs @@ -30,12 +30,6 @@ public static partial class AIFunctionFactory /// The created for invoking . /// /// - /// The resulting exposes metadata about the function via . - /// This metadata includes the function's name, description, and parameters. All of that information may be specified - /// explicitly via ; however, if not specified, defaults are inferred by examining - /// . That includes examining the method and its parameters for s. - /// - /// /// Return values are serialized to using 's /// . Arguments that are not already of the expected type are /// marshaled to the expected type via JSON and using 's @@ -59,13 +53,6 @@ public static AIFunction Create(Delegate method, AIFunctionFactoryCreateOptions? /// The created for invoking . /// /// - /// The resulting exposes metadata about the function via . - /// This metadata includes the function's name, description, and parameters. The function's name and description may - /// be specified explicitly via and , but if they're not, this method - /// will infer values from examining . That includes looking for - /// attributes on the method itself and on its parameters. - /// - /// /// Return values are serialized to using . /// Arguments that are not already of the expected type are marshaled to the expected type via JSON and using /// . If the argument is a , , @@ -102,12 +89,6 @@ public static AIFunction Create(Delegate method, string? name = null, string? de /// The created for invoking . /// /// - /// The resulting exposes metadata about the function via . - /// This metadata includes the function's name, description, and parameters. All of that information may be specified - /// explicitly via ; however, if not specified, defaults are inferred by examining - /// . That includes examining the method and its parameters for s. - /// - /// /// Return values are serialized to using 's /// . Arguments that are not already of the expected type are /// marshaled to the expected type via JSON and using 's @@ -137,13 +118,6 @@ public static AIFunction Create(MethodInfo method, object? target, AIFunctionFac /// The created for invoking . /// /// - /// The resulting exposes metadata about the function via . - /// This metadata includes the function's name, description, and parameters. The function's name and description may - /// be specified explicitly via and , but if they're not, this method - /// will infer values from examining . That includes looking for - /// attributes on the method itself and on its parameters. - /// - /// /// Return values are serialized to using . /// Arguments that are not already of the expected type are marshaled to the expected type via JSON and using /// . If the argument is a , , @@ -257,24 +231,21 @@ static bool IsAsyncMethod(MethodInfo method) _returnMarshaller = GetReturnMarshaller(method, out Type returnType); _returnTypeInfo = returnType != typeof(void) ? options.SerializerOptions.GetTypeInfo(returnType) : null; - string? description = options.Description ?? method.GetCustomAttribute(inherit: true)?.Description; - Metadata = new AIFunctionMetadata(functionName) - { - Description = description, - UnderlyingMethod = method, - AdditionalProperties = options.AdditionalProperties ?? EmptyReadOnlyDictionary.Instance, - JsonSerializerOptions = options.SerializerOptions, - Schema = AIJsonUtilities.CreateFunctionJsonSchema( - method, - title: functionName, - description: description, - options.SerializerOptions, - options.SchemaCreateOptions) - }; + Name = functionName; + Description = options.Description ?? method.GetCustomAttribute(inherit: true)?.Description; + UnderlyingMethod = method; + AdditionalProperties = options.AdditionalProperties ?? EmptyReadOnlyDictionary.Instance; + JsonSerializerOptions = options.SerializerOptions; + JsonSchema = AIJsonUtilities.CreateFunctionJsonSchema( + method, + title: Name, + description: Description, + options.SerializerOptions, + options.JsonSchemaCreateOptions); } /// - public override AIFunctionMetadata Metadata { get; } + public override string Name { get; } /// protected override async Task InvokeCoreAsync( @@ -308,9 +279,9 @@ static bool IsAsyncMethod(MethodInfo method) { case null: Debug.Assert( - Metadata.UnderlyingMethod?.ReturnType == typeof(void) || - Metadata.UnderlyingMethod?.ReturnType == typeof(Task) || - Metadata.UnderlyingMethod?.ReturnType == typeof(ValueTask), "The return parameter should be void or non-generic task."); + UnderlyingMethod?.ReturnType == typeof(void) || + UnderlyingMethod?.ReturnType == typeof(Task) || + UnderlyingMethod?.ReturnType == typeof(ValueTask), "The return parameter should be void or non-generic task."); return null; diff --git a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryCreateOptions.cs b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryCreateOptions.cs index 2043f09c2c6..27db24acadb 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryCreateOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryCreateOptions.cs @@ -16,7 +16,7 @@ namespace Microsoft.Extensions.AI; public sealed class AIFunctionFactoryCreateOptions { private JsonSerializerOptions _options = AIJsonUtilities.DefaultOptions; - private AIJsonSchemaCreateOptions _schemaCreateOptions = AIJsonSchemaCreateOptions.Default; + private AIJsonSchemaCreateOptions _jsonSchemaCreateOptions = AIJsonSchemaCreateOptions.Default; /// /// Initializes a new instance of the class. @@ -35,10 +35,10 @@ public JsonSerializerOptions SerializerOptions /// /// Gets or sets the governing the generation of JSON schemas for the function. /// - public AIJsonSchemaCreateOptions SchemaCreateOptions + public AIJsonSchemaCreateOptions JsonSchemaCreateOptions { - get => _schemaCreateOptions; - set => _schemaCreateOptions = Throw.IfNull(value); + get => _jsonSchemaCreateOptions; + set => _jsonSchemaCreateOptions = Throw.IfNull(value); } /// Gets or sets the name to use for the function. @@ -55,7 +55,7 @@ public AIJsonSchemaCreateOptions SchemaCreateOptions public string? Description { get; set; } /// - /// Gets or sets additional values to store on the resulting property. + /// Gets or sets additional values to store on the resulting property. /// /// /// This property can be used to provide arbitrary information about the function. diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs index 4b1e2679f18..103bc884022 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs @@ -249,11 +249,8 @@ private sealed class NetTypelessAIFunction : AIFunction { public static NetTypelessAIFunction Instance { get; } = new NetTypelessAIFunction(); - public override AIFunctionMetadata Metadata => new("NetTypeless") - { - Description = "AIFunction with parameters that lack .NET types", - }; - + public override string Name => "NetTypeless"; + public override string Description => "AIFunction with parameters that lack .NET types"; protected override Task InvokeCoreAsync(IEnumerable>? arguments, CancellationToken cancellationToken) => Task.FromResult(arguments); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionMetadataTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionMetadataTests.cs deleted file mode 100644 index 9e7f78ab3a2..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionMetadataTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using Xunit; - -namespace Microsoft.Extensions.AI; - -public class AIFunctionMetadataTests -{ - [Fact] - public void Constructor_InvalidArg_Throws() - { - Assert.Throws("name", () => new AIFunctionMetadata((string)null!)); - Assert.Throws("name", () => new AIFunctionMetadata(" \t ")); - Assert.Throws("metadata", () => new AIFunctionMetadata((AIFunctionMetadata)null!)); - } - - [Fact] - public void Constructor_String_PropsDefaulted() - { - AIFunctionMetadata f = new("name"); - Assert.Equal("name", f.Name); - Assert.Empty(f.Description); - Assert.Null(f.UnderlyingMethod); - - Assert.NotNull(f.AdditionalProperties); - Assert.Empty(f.AdditionalProperties); - Assert.Same(f.AdditionalProperties, new AIFunctionMetadata("name2").AdditionalProperties); - } - - [Fact] - public void Constructor_Copy_PropsPropagated() - { - AIFunctionMetadata f1 = new("name") - { - Description = "description", - UnderlyingMethod = typeof(AIFunctionMetadataTests).GetMethod(nameof(Constructor_Copy_PropsPropagated))!, - AdditionalProperties = new Dictionary { { "key", "value" } }, - }; - - AIFunctionMetadata f2 = new(f1); - Assert.Equal(f1.Name, f2.Name); - Assert.Equal(f1.Description, f2.Description); - Assert.Same(f1.UnderlyingMethod, f2.UnderlyingMethod); - Assert.Same(f1.AdditionalProperties, f2.AdditionalProperties); - } - - [Fact] - public void Props_InvalidArg_Throws() - { - Assert.Throws("value", () => new AIFunctionMetadata("name") { Name = null! }); - Assert.Throws("value", () => new AIFunctionMetadata("name") { AdditionalProperties = null! }); - } - - [Fact] - public void Description_NullNormalizedToEmpty() - { - AIFunctionMetadata f = new("name") { Description = null }; - Assert.Equal("", f.Description); - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionTests.cs index df143e8b97e..7502d859038 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionTests.cs @@ -35,7 +35,7 @@ public void ToString_ReturnsName() private sealed class DerivedAIFunction : AIFunction { - public override AIFunctionMetadata Metadata => new("name"); + public override string Name => "name"; protected override Task InvokeCoreAsync(IEnumerable> arguments, CancellationToken cancellationToken) { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs index 3a769af788d..a0804a0451f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -231,10 +231,10 @@ public static void CreateFunctionJsonSchema_ReturnsExpectedValue() JsonSerializerOptions options = new(JsonSerializerOptions.Default); AIFunction func = AIFunctionFactory.Create((int x, int y) => x + y, serializerOptions: options); - Assert.NotNull(func.Metadata.UnderlyingMethod); + Assert.NotNull(func.UnderlyingMethod); - JsonElement resolvedSchema = AIJsonUtilities.CreateFunctionJsonSchema(func.Metadata.UnderlyingMethod, title: func.Metadata.Name); - Assert.True(JsonElement.DeepEquals(resolvedSchema, func.Metadata.Schema)); + JsonElement resolvedSchema = AIJsonUtilities.CreateFunctionJsonSchema(func.UnderlyingMethod, title: func.Name); + Assert.True(JsonElement.DeepEquals(resolvedSchema, func.JsonSchema)); } [Fact] @@ -243,10 +243,9 @@ public static void CreateFunctionJsonSchema_TreatsIntegralTypesAsInteger_EvenWit JsonSerializerOptions options = new(JsonSerializerOptions.Default) { NumberHandling = JsonNumberHandling.AllowReadingFromString }; AIFunction func = AIFunctionFactory.Create((int a, int? b, long c, short d, float e, double f, decimal g) => { }, serializerOptions: options); - AIFunctionMetadata metadata = func.Metadata; - JsonElement schemaParameters = func.Metadata.Schema.GetProperty("properties"); - Assert.NotNull(metadata.UnderlyingMethod); - ParameterInfo[] parameters = metadata.UnderlyingMethod.GetParameters(); + JsonElement schemaParameters = func.JsonSchema.GetProperty("properties"); + Assert.NotNull(func.UnderlyingMethod); + ParameterInfo[] parameters = func.UnderlyingMethod.GetParameters(); Assert.Equal(parameters.Length, schemaParameters.GetPropertyCount()); int i = 0; diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index 1ba067f6d2f..eaf4834e60d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -304,7 +304,7 @@ public virtual async Task FunctionInvocation_RequireSpecific() var response = await chatClient.GetResponseAsync("What's the current secret number?", new() { Tools = [getSecretNumberTool, shieldsUpTool], - ToolMode = ChatToolMode.RequireSpecific(shieldsUpTool.Metadata.Name), + ToolMode = ChatToolMode.RequireSpecific(shieldsUpTool.Name), }); Assert.True(shieldsUp); diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/PromptBasedFunctionCallingChatClient.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/PromptBasedFunctionCallingChatClient.cs index 2a32f39c951..1cf786bb288 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/PromptBasedFunctionCallingChatClient.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/PromptBasedFunctionCallingChatClient.cs @@ -181,9 +181,9 @@ answer the user's question without repeating the same tool call. private static ToolDescriptor ToToolDescriptor(AIFunction tool) => new() { - Name = tool.Metadata.Name, - Description = tool.Metadata.Description, - Arguments = tool.Metadata.UnderlyingMethod?.GetParameters().ToDictionary( + Name = tool.Name, + Description = tool.Description, + Arguments = tool.UnderlyingMethod?.GetParameters().ToDictionary( p => p.Name!, p => new ToolParameterDescriptor { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISerializationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISerializationTests.cs index 66dd1212abc..51947ae0c8e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISerializationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISerializationTests.cs @@ -370,13 +370,13 @@ public static async Task RequestDeserialization_ToolCall() Assert.NotNull(request.Options.Tools); AIFunction function = Assert.IsAssignableFrom(Assert.Single(request.Options.Tools)); - Assert.Equal("Gets the age of the specified person.", function.Metadata.Description); - Assert.Equal("GetPersonAge", function.Metadata.Name); - Assert.Equal("Strict", Assert.Single(function.Metadata.AdditionalProperties).Key); + Assert.Equal("Gets the age of the specified person.", function.Description); + Assert.Equal("GetPersonAge", function.Name); + Assert.Equal("Strict", Assert.Single(function.AdditionalProperties).Key); - Assert.Null(function.Metadata.UnderlyingMethod); + Assert.Null(function.UnderlyingMethod); - JsonObject parametersSchema = Assert.IsType(JsonNode.Parse(function.Metadata.Schema.GetProperty("properties").GetRawText())); + JsonObject parametersSchema = Assert.IsType(JsonNode.Parse(function.JsonSchema.GetProperty("properties").GetRawText())); var parameterSchema = Assert.IsType(Assert.Single(parametersSchema.Select(kvp => kvp.Value))); Assert.Equal(2, parameterSchema.Count); Assert.Equal("The person whose age is being requested", (string)parameterSchema["description"]!); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index 72b2ca93ed8..b8a351e1c5a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -363,8 +363,8 @@ public async Task RejectsMultipleChoicesAsync() var expected = new ChatResponse( [ - new(ChatRole.Assistant, [new FunctionCallContent("callId1", func1.Metadata.Name)]), - new(ChatRole.Assistant, [new FunctionCallContent("callId2", func2.Metadata.Name)]), + new(ChatRole.Assistant, [new FunctionCallContent("callId1", func1.Name)]), + new(ChatRole.Assistant, [new FunctionCallContent("callId2", func2.Name)]), ]); using var innerClient = new TestChatClient diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index b0016e0adc1..d8673e36c4d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -135,22 +135,22 @@ public void Metadata_DerivedFromLambda() Func dotnetFunc = () => "test"; func = AIFunctionFactory.Create(dotnetFunc); - Assert.Contains("Metadata_DerivedFromLambda", func.Metadata.Name); - Assert.Empty(func.Metadata.Description); - Assert.Same(dotnetFunc.Method, func.Metadata.UnderlyingMethod); + Assert.Contains("Metadata_DerivedFromLambda", func.Name); + Assert.Empty(func.Description); + Assert.Same(dotnetFunc.Method, func.UnderlyingMethod); Func dotnetFunc2 = (string a) => a + " " + a; func = AIFunctionFactory.Create(dotnetFunc2); - Assert.Contains("Metadata_DerivedFromLambda", func.Metadata.Name); - Assert.Empty(func.Metadata.Description); - Assert.Same(dotnetFunc2.Method, func.Metadata.UnderlyingMethod); + Assert.Contains("Metadata_DerivedFromLambda", func.Name); + Assert.Empty(func.Description); + Assert.Same(dotnetFunc2.Method, func.UnderlyingMethod); Func dotnetFunc3 = [Description("This is a test function")] ([Description("This is A")] string a, [Description("This is B")] string b) => b + " " + a; func = AIFunctionFactory.Create(dotnetFunc3); - Assert.Contains("Metadata_DerivedFromLambda", func.Metadata.Name); - Assert.Equal("This is a test function", func.Metadata.Description); - Assert.Same(dotnetFunc3.Method, func.Metadata.UnderlyingMethod); - Assert.Collection(func.Metadata.UnderlyingMethod!.GetParameters(), + Assert.Contains("Metadata_DerivedFromLambda", func.Name); + Assert.Equal("This is a test function", func.Description); + Assert.Same(dotnetFunc3.Method, func.UnderlyingMethod); + Assert.Collection(func.UnderlyingMethod!.GetParameters(), p => Assert.Equal("This is A", p.GetCustomAttribute()?.Description), p => Assert.Equal("This is B", p.GetCustomAttribute()?.Description)); } @@ -174,17 +174,17 @@ public void AIFunctionFactoryCreateOptions_ValuesPropagateToAIFunction() Action dotnetFunc = () => { }; AIFunction func = AIFunctionFactory.Create(dotnetFunc, options); - Assert.Equal("test name", func.Metadata.Name); - Assert.Equal("test description", func.Metadata.Description); - Assert.Same(dotnetFunc.Method, func.Metadata.UnderlyingMethod); - Assert.Equal(metadata, func.Metadata.AdditionalProperties); + Assert.Equal("test name", func.Name); + Assert.Equal("test description", func.Description); + Assert.Same(dotnetFunc.Method, func.UnderlyingMethod); + Assert.Equal(metadata, func.AdditionalProperties); } [Fact] public void AIFunctionFactoryCreateOptions_SchemaOptions_HasExpectedDefaults() { var options = new AIFunctionFactoryCreateOptions(); - var schemaOptions = options.SchemaCreateOptions; + var schemaOptions = options.JsonSchemaCreateOptions; Assert.NotNull(schemaOptions); Assert.True(schemaOptions.IncludeTypeInEnumSchemas); From 2760a67c5347c572bf48f5280287c962ad7df406 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 13 Feb 2025 19:02:46 +0000 Subject: [PATCH 7/9] Address feedback. --- .../Functions/AIFunction.cs | 6 ++---- .../Utilities/AIJsonUtilities.Schema.cs | 12 ++++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs index b45a829e79b..1ef970c81aa 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs @@ -39,12 +39,10 @@ public virtual string Description } /// - /// Gets a for the underlying .NET method this represents. + /// Gets the underlying that this might be wrapping. /// /// - /// This property provides additional metadata on the function and its signature. - /// Setting this property is optional and should have no impact on function invocation or its JSON schema, - /// which is how implementations interface with AI functions primarily. + /// Provides additional metadata on the function and its signature. Implementations not wrapping .NET methods may return . /// public virtual MethodInfo? UnderlyingMethod { get; init; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs index 1706b2c00c8..805c121b326 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs @@ -48,28 +48,28 @@ public static partial class AIJsonUtilities /// /// Determines a JSON schema for the provided method. /// - /// The method from which to extract schema information. + /// The method from which to extract schema information. /// The title keyword used by the method schema. /// The description keyword used by the method schema. /// The options used to extract the schema from the specified type. /// The options controlling schema inference. /// A JSON schema document encoded as a . public static JsonElement CreateFunctionJsonSchema( - MethodBase methodInfo, + MethodBase method, string? title = null, string? description = null, JsonSerializerOptions? serializerOptions = null, AIJsonSchemaCreateOptions? inferenceOptions = null) { - _ = Throw.IfNull(methodInfo); + _ = Throw.IfNull(method); serializerOptions ??= DefaultOptions; inferenceOptions ??= AIJsonSchemaCreateOptions.Default; - title ??= methodInfo.Name; - description ??= methodInfo.GetCustomAttribute()?.Description; + title ??= method.Name; + description ??= method.GetCustomAttribute()?.Description; JsonObject parameterSchemas = new(); JsonArray? requiredProperties = null; - foreach (ParameterInfo parameter in methodInfo.GetParameters()) + foreach (ParameterInfo parameter in method.GetParameters()) { if (string.IsNullOrWhiteSpace(parameter.Name)) { From 6c5be61d535f24c11b4c234935f6aebb1c687463 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 13 Feb 2025 19:07:13 +0000 Subject: [PATCH 8/9] Mark JSO as virtual --- .../Functions/AIFunction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs index 1ef970c81aa..41bae9e2db6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs @@ -89,7 +89,7 @@ public virtual JsonElement JsonSchema } /// Gets a that can be used to marshal function parameters. - public JsonSerializerOptions? JsonSerializerOptions { get; init; } + public virtual JsonSerializerOptions? JsonSerializerOptions { get; init; } /// Invokes the and returns its result. /// The arguments to pass to the function's invocation. From 24688f1cef7ee97bbea4e0f77c72f9e25d2e78b0 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 13 Feb 2025 20:12:02 +0000 Subject: [PATCH 9/9] Remove init methods from AIFunction properties. --- .../Functions/AIFunction.cs | 53 +++++-------------- .../OpenAIModelMapper.ChatCompletion.cs | 13 +++-- .../Functions/AIFunctionFactory.cs | 8 ++- .../Functions/AIFunctionTests.cs | 1 + 4 files changed, 25 insertions(+), 50 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs index 41bae9e2db6..84cde4bc82a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs @@ -1,16 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Collections; -using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -18,33 +15,11 @@ namespace Microsoft.Extensions.AI; [DebuggerDisplay("{DebuggerDisplay,nq}")] public abstract class AIFunction : AITool { - /// The description of the function. - private readonly string _description = string.Empty; - - /// The JSON schema describing the function and its input parameters. - private readonly JsonElement _jsonSchema = AIJsonUtilities.DefaultJsonSchema; - - /// Optional additional properties in addition to the named properties already available on this class. - private readonly IReadOnlyDictionary _additionalProperties = EmptyReadOnlyDictionary.Instance; - /// Gets the name of the function. public abstract string Name { get; } /// Gets a description of the function, suitable for use in describing the purpose to a model. - [AllowNull] - public virtual string Description - { - get => _description; - init => _description = value ?? string.Empty; - } - - /// - /// Gets the underlying that this might be wrapping. - /// - /// - /// Provides additional metadata on the function and its signature. Implementations not wrapping .NET methods may return . - /// - public virtual MethodInfo? UnderlyingMethod { get; init; } + public abstract string Description { get; } /// Gets a JSON Schema describing the function and its input parameters. /// @@ -71,25 +46,21 @@ public virtual string Description /// When no schema is specified, consuming chat clients should assume the "{}" or "true" schema, indicating that any JSON input is admissible. /// /// - public virtual JsonElement JsonSchema - { - get => _jsonSchema; - init - { - AIJsonUtilities.ValidateSchemaDocument(value); - _jsonSchema = value; - } - } + public virtual JsonElement JsonSchema => AIJsonUtilities.DefaultJsonSchema; + + /// + /// Gets the underlying that this might be wrapping. + /// + /// + /// Provides additional metadata on the function and its signature. Implementations not wrapping .NET methods may return . + /// + public virtual MethodInfo? UnderlyingMethod => null; /// Gets any additional properties associated with the function. - public virtual IReadOnlyDictionary AdditionalProperties - { - get => _additionalProperties; - init => _additionalProperties = Throw.IfNull(value); - } + public virtual IReadOnlyDictionary AdditionalProperties => EmptyReadOnlyDictionary.Instance; /// Gets a that can be used to marshal function parameters. - public virtual JsonSerializerOptions? JsonSerializerOptions { get; init; } + public virtual JsonSerializerOptions? JsonSerializerOptions => AIJsonUtilities.DefaultOptions; /// Invokes the and returns its result. /// The arguments to pass to the function's invocation. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs index f18c76a137e..c1e0189c8cd 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs @@ -416,17 +416,16 @@ private static AITool FromOpenAIChatTool(ChatTool chatTool) } OpenAIChatToolJson openAiChatTool = JsonSerializer.Deserialize(chatTool.FunctionParameters.ToMemory().Span, OpenAIJsonContext.Default.OpenAIChatToolJson)!; - return new MetadataOnlyAIFunction(chatTool.FunctionName) - { - Description = chatTool.FunctionDescription, - AdditionalProperties = additionalProperties, - JsonSchema = JsonSerializer.SerializeToElement(openAiChatTool, OpenAIJsonContext.Default.OpenAIChatToolJson), - }; + JsonElement schema = JsonSerializer.SerializeToElement(openAiChatTool, OpenAIJsonContext.Default.OpenAIChatToolJson); + return new MetadataOnlyAIFunction(chatTool.FunctionName, chatTool.FunctionDescription, schema, additionalProperties); } - private sealed class MetadataOnlyAIFunction(string name) : AIFunction + private sealed class MetadataOnlyAIFunction(string name, string description, JsonElement schema, IReadOnlyDictionary additionalProps) : AIFunction { public override string Name => name; + public override string Description => description; + public override JsonElement JsonSchema => schema; + public override IReadOnlyDictionary AdditionalProperties => additionalProps; protected override Task InvokeCoreAsync(IEnumerable> arguments, CancellationToken cancellationToken) => throw new InvalidOperationException($"The AI function '{Name}' does not support being invoked."); } diff --git a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs index c7aef70c88b..8f473fe3f7a 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs @@ -232,7 +232,7 @@ static bool IsAsyncMethod(MethodInfo method) _returnTypeInfo = returnType != typeof(void) ? options.SerializerOptions.GetTypeInfo(returnType) : null; Name = functionName; - Description = options.Description ?? method.GetCustomAttribute(inherit: true)?.Description; + Description = options.Description ?? method.GetCustomAttribute(inherit: true)?.Description ?? string.Empty; UnderlyingMethod = method; AdditionalProperties = options.AdditionalProperties ?? EmptyReadOnlyDictionary.Instance; JsonSerializerOptions = options.SerializerOptions; @@ -244,8 +244,12 @@ static bool IsAsyncMethod(MethodInfo method) options.JsonSchemaCreateOptions); } - /// public override string Name { get; } + public override string Description { get; } + public override MethodInfo? UnderlyingMethod { get; } + public override IReadOnlyDictionary AdditionalProperties { get; } + public override JsonSerializerOptions JsonSerializerOptions { get; } + public override JsonElement JsonSchema { get; } /// protected override async Task InvokeCoreAsync( diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionTests.cs index 7502d859038..1ced6ae3185 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionTests.cs @@ -36,6 +36,7 @@ public void ToString_ReturnsName() private sealed class DerivedAIFunction : AIFunction { public override string Name => "name"; + public override string Description => ""; protected override Task InvokeCoreAsync(IEnumerable> arguments, CancellationToken cancellationToken) {