File: ChatCompletion\ChatResponseFormat.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.AI.Abstractions\Microsoft.Extensions.AI.Abstractions.csproj (Microsoft.Extensions.AI.Abstractions)
// 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.ComponentModel;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Microsoft.Shared.Diagnostics;
 
namespace Microsoft.Extensions.AI;
 
#pragma warning disable CA1052 // Static holder types should be Static or NotInheritable
#pragma warning disable S2333 // gratuitous partial
 
/// <summary>Represents the response format that is desired by the caller.</summary>
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(ChatResponseFormatText), typeDiscriminator: "text")]
[JsonDerivedType(typeof(ChatResponseFormatJson), typeDiscriminator: "json")]
public partial class ChatResponseFormat
{
    private static readonly AIJsonSchemaCreateOptions _inferenceOptions = new()
    {
        IncludeSchemaKeyword = true,
    };
 
    /// <summary>Initializes a new instance of the <see cref="ChatResponseFormat"/> class.</summary>
    /// <remarks>Prevents external instantiation. Close the inheritance hierarchy for now until we have good reason to open it.</remarks>
    private protected ChatResponseFormat()
    {
    }
 
    /// <summary>Gets a singleton instance representing unstructured textual data.</summary>
    public static ChatResponseFormatText Text { get; } = new();
 
    /// <summary>Gets a singleton instance representing structured JSON data but without any particular schema.</summary>
    public static ChatResponseFormatJson Json { get; } = new(schema: null);
 
    /// <summary>Creates a <see cref="ChatResponseFormatJson"/> representing structured JSON data with the specified schema.</summary>
    /// <param name="schema">The JSON schema.</param>
    /// <param name="schemaName">An optional name of the schema. For example, if the schema represents a particular class, this could be the name of the class.</param>
    /// <param name="schemaDescription">An optional description of the schema.</param>
    /// <returns>The <see cref="ChatResponseFormatJson"/> instance.</returns>
    public static ChatResponseFormatJson ForJsonSchema(
        JsonElement schema, string? schemaName = null, string? schemaDescription = null) =>
        new(schema, schemaName, schemaDescription);
 
    /// <summary>Creates a <see cref="ChatResponseFormatJson"/> representing structured JSON data with a schema based on <typeparamref name="T"/>.</summary>
    /// <typeparam name="T">The type for which a schema should be exported and used as the response schema.</typeparam>
    /// <param name="serializerOptions">The JSON serialization options to use.</param>
    /// <param name="schemaName">An optional name of the schema. By default, this will be inferred from <typeparamref name="T"/>.</param>
    /// <param name="schemaDescription">An optional description of the schema. By default, this will be inferred from <typeparamref name="T"/>.</param>
    /// <returns>The <see cref="ChatResponseFormatJson"/> instance.</returns>
    /// <remarks>
    /// Many AI services that support structured output require that the JSON schema have a top-level 'type=object'.
    /// If <typeparamref name="T"/> is a primitive type like <see cref="string"/>, <see cref="int"/>, or <see cref="bool"/>,
    /// or if it's a type that serializes as a JSON array, attempting to use the resulting schema with such services may fail.
    /// In such cases, consider instead using a <typeparamref name="T"/> that wraps the actual type in a class or struct so that
    /// it serializes as a JSON object with the original type as a property of that object.
    /// </remarks>
    public static ChatResponseFormatJson ForJsonSchema<T>(
        JsonSerializerOptions? serializerOptions = null, string? schemaName = null, string? schemaDescription = null) =>
        ForJsonSchema(typeof(T), serializerOptions, schemaName, schemaDescription);
 
    /// <summary>Creates a <see cref="ChatResponseFormatJson"/> representing structured JSON data with a schema based on <paramref name="schemaType"/>.</summary>
    /// <param name="schemaType">The <see cref="Type"/> for which a schema should be exported and used as the response schema.</param>
    /// <param name="serializerOptions">The JSON serialization options to use.</param>
    /// <param name="schemaName">An optional name of the schema. By default, this will be inferred from <paramref name="schemaType"/>.</param>
    /// <param name="schemaDescription">An optional description of the schema. By default, this will be inferred from <paramref name="schemaType"/>.</param>
    /// <returns>The <see cref="ChatResponseFormatJson"/> instance.</returns>
    /// <remarks>
    /// Many AI services that support structured output require that the JSON schema have a top-level 'type=object'.
    /// If <paramref name="schemaType"/> is a primitive type like <see cref="string"/>, <see cref="int"/>, or <see cref="bool"/>,
    /// or if it's a type that serializes as a JSON array, attempting to use the resulting schema with such services may fail.
    /// In such cases, consider instead using a <paramref name="schemaType"/> that wraps the actual type in a class or struct so that
    /// it serializes as a JSON object with the original type as a property of that object.
    /// </remarks>
    /// <exception cref="ArgumentNullException"><paramref name="schemaType"/> is <see langword="null"/>.</exception>
    public static ChatResponseFormatJson ForJsonSchema(
        Type schemaType, JsonSerializerOptions? serializerOptions = null, string? schemaName = null, string? schemaDescription = null)
    {
        _ = Throw.IfNull(schemaType);
 
        var schema = AIJsonUtilities.CreateJsonSchema(
            schemaType,
            serializerOptions: serializerOptions ?? AIJsonUtilities.DefaultOptions,
            inferenceOptions: _inferenceOptions);
 
        return ForJsonSchema(
            schema,
            schemaName ?? InvalidNameCharsRegex().Replace(schemaType.Name, "_"),
            schemaDescription ?? schemaType.GetCustomAttribute<DescriptionAttribute>()?.Description);
    }
 
    /// <summary>Regex that flags any character other than ASCII digits, ASCII letters, or underscore.</summary>
#if NET
    [GeneratedRegex("[^0-9A-Za-z_]")]
    private static partial Regex InvalidNameCharsRegex();
#else
    private static Regex InvalidNameCharsRegex() => _invalidNameCharsRegex;
    private static readonly Regex _invalidNameCharsRegex = new("[^0-9A-Za-z_]", RegexOptions.Compiled);
#endif
}