Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,18 +25,9 @@ public sealed class AIFunctionMetadata
/// <summary>The JSON schema describing the function and its input parameters.</summary>
private readonly JsonElement _schema = AIJsonUtilities.DefaultJsonSchema;

/// <summary>The function's parameters.</summary>
private readonly IReadOnlyList<AIFunctionParameterMetadata> _parameters = [];

/// <summary>The function's return parameter.</summary>
private readonly AIFunctionReturnParameterMetadata _returnParameter = AIFunctionReturnParameterMetadata.Empty;

/// <summary>Optional additional properties in addition to the named properties already available on this class.</summary>
private readonly IReadOnlyDictionary<string, object?> _additionalProperties = EmptyReadOnlyDictionary<string, object?>.Instance;

/// <summary><see cref="_parameters"/> indexed by name, lazily initialized.</summary>
private Dictionary<string, AIFunctionParameterMetadata>? _parametersByName;

/// <summary>Initializes a new instance of the <see cref="AIFunctionMetadata"/> class for a function with the specified name.</summary>
/// <param name="name">The name of the function.</param>
/// <exception cref="ArgumentNullException">The <paramref name="name"/> was null.</exception>
Expand All @@ -48,15 +39,13 @@ public AIFunctionMetadata(string name)
/// <summary>Initializes a new instance of the <see cref="AIFunctionMetadata"/> class as a copy of another <see cref="AIFunctionMetadata"/>.</summary>
/// <exception cref="ArgumentNullException">The <paramref name="metadata"/> was null.</exception>
/// <remarks>
/// This creates a shallow clone of <paramref name="metadata"/>. The new instance's <see cref="Parameters"/> and
/// <see cref="ReturnParameter"/> properties will return the same objects as in the original instance.
/// This creates a shallow clone of <paramref name="metadata"/>.
/// </remarks>
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;
}
Expand All @@ -76,33 +65,15 @@ public string Description
init => _description = value ?? string.Empty;
}

/// <summary>Gets the metadata for the parameters to the function.</summary>
/// <remarks>If the function has no parameters, the returned list is empty.</remarks>
public IReadOnlyList<AIFunctionParameterMetadata> Parameters
{
get => _parameters;
init => _parameters = Throw.IfNull(value);
}

/// <summary>Gets the <see cref="AIFunctionParameterMetadata"/> for a parameter by its name.</summary>
/// <param name="name">The name of the parameter.</param>
/// <returns>The corresponding <see cref="AIFunctionParameterMetadata"/>, if found; otherwise, null.</returns>
public AIFunctionParameterMetadata? GetParameter(string name)
{
Dictionary<string, AIFunctionParameterMetadata>? parametersByName = _parametersByName ??= _parameters.ToDictionary(p => p.Name);

return parametersByName.TryGetValue(name, out AIFunctionParameterMetadata? parameter) ?
parameter :
null;
}

/// <summary>Gets parameter metadata for the return parameter.</summary>
/// <remarks>If the function has no return parameter, the value is a default instance of an <see cref="AIFunctionReturnParameterMetadata"/>.</remarks>
public AIFunctionReturnParameterMetadata ReturnParameter
{
get => _returnParameter;
init => _returnParameter = Throw.IfNull(value);
}
/// <summary>
/// Gets a <see cref="MethodInfo"/> for the underlying .NET method this <see cref="AIFunction"/> represents.
/// </summary>
/// <remarks>
/// 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 <see cref="IChatClient"/> implementations interface with AI functions primarily.
/// </remarks>
public MethodInfo? UnderlyingMethod { get; init; }

/// <summary>Gets a JSON Schema describing the function and its input parameters.</summary>
/// <remarks>
Expand All @@ -124,8 +95,6 @@ public AIFunctionReturnParameterMetadata ReturnParameter
/// </code>
/// <para>
/// 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 <see cref="Parameters"/> list.
/// </para>
/// <para>
/// When no schema is specified, consuming chat clients should assume the "{}" or "true" schema, indicating that any JSON input is admissible.
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -46,39 +46,47 @@ public static partial class AIJsonUtilities
private static readonly string[] _schemaKeywordsDisallowedByAIVendors = ["minLength", "maxLength", "pattern", "format"];

/// <summary>
/// Determines a JSON schema for the provided AI function parameter metadata.
/// Determines a JSON schema for the provided method signature.
/// </summary>
/// <param name="methodInfo">The method from which to extract schema information.</param>
/// <param name="title">The title keyword used by the method schema.</param>
/// <param name="description">The description keyword used by the method schema.</param>
/// <param name="parameters">The AI function parameter metadata.</param>
/// <param name="serializerOptions">The options used to extract the schema from the specified type.</param>
/// <param name="inferenceOptions">The options controlling schema inference.</param>
/// <returns>A JSON schema document encoded as a <see cref="JsonElement"/>.</returns>
public static JsonElement CreateFunctionJsonSchema(
MethodBase methodInfo,
string? title = null,
string? description = null,
IReadOnlyList<AIFunctionParameterMetadata>? parameters = null,
JsonSerializerOptions? serializerOptions = null,
AIJsonSchemaCreateOptions? inferenceOptions = null)
{
_ = Throw.IfNull(methodInfo);
serializerOptions ??= DefaultOptions;
inferenceOptions ??= AIJsonSchemaCreateOptions.Default;
title ??= methodInfo.Name;
description ??= methodInfo.GetCustomAttribute<DescriptionAttribute>()?.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<DescriptionAttribute>(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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -418,26 +416,11 @@ private static AITool FromOpenAIChatTool(ChatTool chatTool)
}

OpenAIChatToolJson openAiChatTool = JsonSerializer.Deserialize(chatTool.FunctionParameters.ToMemory().Span, OpenAIJsonContext.Default.OpenAIChatToolJson)!;
List<AIFunctionParameterMetadata> parameters = new(openAiChatTool.Properties.Count);
foreach (KeyValuePair<string, JsonElement> 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);
Expand Down
Loading