diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs index a4b5ecb5378..84cde4bc82a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Reflection; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Collections; @@ -13,8 +15,52 @@ namespace Microsoft.Extensions.AI; [DebuggerDisplay("{DebuggerDisplay,nq}")] public abstract class AIFunction : AITool { - /// Gets metadata describing the function. - public abstract AIFunctionMetadata Metadata { get; } + /// 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. + public abstract string Description { get; } + + /// 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 => 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 => EmptyReadOnlyDictionary.Instance; + + /// Gets a that can be used to marshal function parameters. + public virtual JsonSerializerOptions? JsonSerializerOptions => AIJsonUtilities.DefaultOptions; /// Invokes the and returns its result. /// The arguments to pass to the function's invocation. @@ -30,7 +76,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 +88,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 d6e5279d589..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionMetadata.cs +++ /dev/null @@ -1,153 +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.Linq; -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; - - /// 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. - 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 . The new instance's and - /// properties will return the same objects as in the original instance. - /// - public AIFunctionMetadata(AIFunctionMetadata metadata) - { - Name = Throw.IfNull(metadata).Name; - Description = metadata.Description; - Parameters = metadata.Parameters; - ReturnParameter = metadata.ReturnParameter; - 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 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 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. - /// 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. - /// - /// - 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.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..805c121b326 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. /// + /// 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 method, string? title = null, string? description = null, - IReadOnlyList? parameters = null, JsonSerializerOptions? serializerOptions = null, AIJsonSchemaCreateOptions? inferenceOptions = null) { + _ = Throw.IfNull(method); serializerOptions ??= DefaultOptions; inferenceOptions ??= AIJsonSchemaCreateOptions.Default; + title ??= method.Name; + description ??= method.GetCustomAttribute()?.Description; JsonObject parameterSchemas = new(); JsonArray? requiredProperties = null; - foreach (AIFunctionParameterMetadata parameter in parameters ?? []) + foreach (ParameterInfo parameter in method.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.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 170f8cbe06e..c1e0189c8cd 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,50 +416,32 @@ 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); + JsonElement schema = JsonSerializer.SerializeToElement(openAiChatTool, OpenAIJsonContext.Default.OpenAIChatToolJson); + return new MetadataOnlyAIFunction(chatTool.FunctionName, chatTool.FunctionDescription, schema, additionalProperties); } - private sealed class MetadataOnlyAIFunction(AIFunctionMetadata metadata) : AIFunction + private sealed class MetadataOnlyAIFunction(string name, string description, JsonElement schema, IReadOnlyDictionary additionalProps) : AIFunction { - public override AIFunctionMetadata Metadata => metadata; + 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 '{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 a3084882d75..8f473fe3f7a 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 , , @@ -242,53 +216,40 @@ 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), - }, - AdditionalProperties = options.AdditionalProperties ?? EmptyReadOnlyDictionary.Instance, - JsonSerializerOptions = options.SerializerOptions, - Schema = AIJsonUtilities.CreateFunctionJsonSchema( - title: functionName, - description: description, - parameters: options.Parameters ?? parameterMetadata, - options.SerializerOptions, - options.SchemaCreateOptions) - }; + Name = functionName; + Description = options.Description ?? method.GetCustomAttribute(inherit: true)?.Description ?? string.Empty; + 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; } + 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( @@ -321,7 +282,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( + UnderlyingMethod?.ReturnType == typeof(void) || + UnderlyingMethod?.ReturnType == typeof(Task) || + UnderlyingMethod?.ReturnType == typeof(ValueTask), "The return parameter should be void or non-generic task."); + return null; case { Kind: JsonTypeInfoKind.None }: @@ -342,11 +307,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 +327,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 +339,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 +380,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 +418,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 +431,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..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. @@ -54,20 +54,8 @@ 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. + /// 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 747708602cd..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,20 +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", - Parameters = - [ - new AIFunctionParameterMetadata("a"), - new AIFunctionParameterMetadata("b"), - new AIFunctionParameterMetadata("c"), - new AIFunctionParameterMetadata("d"), - new AIFunctionParameterMetadata("e"), - new AIFunctionParameterMetadata("f"), - ] - }; - + 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 08397144632..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionMetadataTests.cs +++ /dev/null @@ -1,98 +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.Text.Json; -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.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.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", - Parameters = [new AIFunctionParameterMetadata("param")], - ReturnParameter = new AIFunctionReturnParameterMetadata(), - 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.AdditionalProperties, f2.AdditionalProperties); - } - - [Fact] - 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! }); - } - - [Fact] - 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/Functions/AIFunctionTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/AIFunctionTests.cs index df143e8b97e..1ced6ae3185 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,8 @@ public void ToString_ReturnsName() private sealed class DerivedAIFunction : AIFunction { - public override AIFunctionMetadata Metadata => new("name"); + public override string Name => "name"; + public override string Description => ""; 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 4d654e042ff..a0804a0451f 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,11 +231,10 @@ 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.UnderlyingMethod); - JsonElement resolvedSchema = AIJsonUtilities.CreateFunctionJsonSchema(title: func.Metadata.Name, description: func.Metadata.Description, parameters: func.Metadata.Parameters); - 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,14 +243,15 @@ 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.Equal(metadata.Parameters.Count, schemaParameters.GetPropertyCount()); + JsonElement schemaParameters = func.JsonSchema.GetProperty("properties"); + Assert.NotNull(func.UnderlyingMethod); + ParameterInfo[] parameters = func.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/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 b442fa74b40..1cf786bb288 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; @@ -179,17 +181,17 @@ 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.Parameters.ToDictionary( - p => p.Name, + Name = tool.Name, + Description = tool.Description, + Arguments = tool.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, + }) ?? [], }; private sealed class ToolDescriptor diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISerializationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISerializationTests.cs index 205229f0cfd..51947ae0c8e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISerializationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISerializationTests.cs @@ -370,17 +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("Return parameter", function.Metadata.ReturnParameter.Description); - Assert.Equal("{}", Assert.IsType(function.Metadata.ReturnParameter.Schema).GetRawText()); + Assert.Equal("Gets the age of the specified person.", function.Description); + Assert.Equal("GetPersonAge", function.Name); + Assert.Equal("Strict", Assert.Single(function.AdditionalProperties).Key); - AIFunctionParameterMetadata parameter = Assert.Single(function.Metadata.Parameters); - Assert.Equal("personName", parameter.Name); - Assert.True(parameter.IsRequired); + 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 207a4705751..d8673e36c4d 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,62 +133,58 @@ public void Metadata_DerivedFromLambda() { AIFunction func; - func = AIFunctionFactory.Create(() => "test"); - 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); - - func = AIFunctionFactory.Create((string a) => a + " " + a); - Assert.Contains("Metadata_DerivedFromLambda", func.Metadata.Name); - Assert.Empty(func.Metadata.Description); - Assert.Single(func.Metadata.Parameters); - - func = AIFunctionFactory.Create( - [Description("This is a test function")] ([Description("This is A")] string a, [Description("This is B")] string b) => b + " " + a); - 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)); + Func dotnetFunc = () => "test"; + func = AIFunctionFactory.Create(dotnetFunc); + 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.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.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)); } [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.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);