Skip to content

Commit

Permalink
Expose a schema transformer on AIJsonSchemaCreateOptions. (dotnet#5677)
Browse files Browse the repository at this point in the history
* Expose a schema transformer on AIJsonSchemaCreateOptions.

* Address feedback

* Disable caching if a transformer is specified.

* Remove `FilterDisallowedKeywords`.

* Document caching.

* Apply suggestions from code review
  • Loading branch information
eiriktsarpalis authored and stephentoub committed Nov 20, 2024
1 parent 1f47a84 commit 7f2d900
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<InjectSharedEmptyCollections>true</InjectSharedEmptyCollections>
<InjectStringHashOnLegacy>true</InjectStringHashOnLegacy>
<InjectStringSyntaxAttributeOnLegacy>true</InjectStringSyntaxAttributeOnLegacy>
<InjectRequiredMemberOnLegacy>true</InjectRequiredMemberOnLegacy>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// 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.Linq;
using System.Reflection;
using System.Text.Json.Schema;
using System.Text.Json.Serialization.Metadata;

#pragma warning disable CA1815 // Override equals and operator equals on value types

namespace Microsoft.Extensions.AI;

/// <summary>
/// Defines the context in which a JSON schema within a type graph is being generated.
/// </summary>
/// <remarks>
/// This struct is being passed to the user-provided <see cref="AIJsonSchemaCreateOptions.TransformSchemaNode"/>
/// callback by the <see cref="AIJsonUtilities.CreateJsonSchema"/> method and cannot be instantiated directly.
/// </remarks>
public readonly struct AIJsonSchemaCreateContext
{
private readonly JsonSchemaExporterContext _exporterContext;

internal AIJsonSchemaCreateContext(JsonSchemaExporterContext exporterContext)
{
_exporterContext = exporterContext;
}

/// <summary>
/// Gets the path to the schema document currently being generated.
/// </summary>
public ReadOnlySpan<string> Path => _exporterContext.Path;

/// <summary>
/// Gets the <see cref="JsonTypeInfo"/> for the type being processed.
/// </summary>
public JsonTypeInfo TypeInfo => _exporterContext.TypeInfo;

/// <summary>
/// Gets the type info for the polymorphic base type if generated as a derived type.
/// </summary>
public JsonTypeInfo? BaseTypeInfo => _exporterContext.BaseTypeInfo;

/// <summary>
/// Gets the <see cref="JsonPropertyInfo"/> if the schema is being generated for a property.
/// </summary>
public JsonPropertyInfo? PropertyInfo => _exporterContext.PropertyInfo;

/// <summary>
/// Gets the declaring type of the property or parameter being processed.
/// </summary>
public Type? DeclaringType =>
#if NET9_0_OR_GREATER
_exporterContext.PropertyInfo?.DeclaringType;
#else
_exporterContext.DeclaringType;
#endif

/// <summary>
/// Gets the <see cref="ICustomAttributeProvider"/> corresponding to the property or field being processed.
/// </summary>
public ICustomAttributeProvider? PropertyAttributeProvider =>
#if NET9_0_OR_GREATER
_exporterContext.PropertyInfo?.AttributeProvider;
#else
_exporterContext.PropertyAttributeProvider;
#endif

/// <summary>
/// Gets the <see cref="System.Reflection.ICustomAttributeProvider"/> of the
/// constructor parameter associated with the accompanying <see cref="PropertyInfo"/>.
/// </summary>
public ICustomAttributeProvider? ParameterAttributeProvider =>
#if NET9_0_OR_GREATER
_exporterContext.PropertyInfo?.AssociatedParameter?.AttributeProvider;
#else
_exporterContext.ParameterInfo;
#endif

/// <summary>
/// Retrieves a custom attribute of a specified type that is applied to the specified schema node context.
/// </summary>
/// <typeparam name="TAttribute">The type of attribute to search for.</typeparam>
/// <param name="inherit">If <see langword="true"/>, specifies to also search the ancestors of the context members for custom attributes.</param>
/// <returns>The first occurrence of <typeparamref name="TAttribute"/> if found, or <see langword="null"/> otherwise.</returns>
/// <remarks>
/// This helper method resolves attributes from context locations in the following order:
/// <list type="number">
/// <item>Attributes specified on the property of the context, if specified.</item>
/// <item>Attributes specified on the constructor parameter of the context, if specified.</item>
/// <item>Attributes specified on the type of the context.</item>
/// </list>
/// </remarks>
public TAttribute? GetCustomAttribute<TAttribute>(bool inherit = false)
where TAttribute : Attribute
{
return GetCustomAttr(PropertyAttributeProvider) ??
GetCustomAttr(ParameterAttributeProvider) ??
GetCustomAttr(TypeInfo.Type);

TAttribute? GetCustomAttr(ICustomAttributeProvider? provider) =>
(TAttribute?)provider?.GetCustomAttributes(typeof(TAttribute), inherit).FirstOrDefault();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// 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.Nodes;

namespace Microsoft.Extensions.AI;

/// <summary>
Expand All @@ -13,6 +16,11 @@ public sealed class AIJsonSchemaCreateOptions
/// </summary>
public static AIJsonSchemaCreateOptions Default { get; } = new AIJsonSchemaCreateOptions();

/// <summary>
/// Gets a callback that is invoked for every schema that is generated within the type graph.
/// </summary>
public Func<AIJsonSchemaCreateContext, JsonNode, JsonNode>? TransformSchemaNode { get; init; }

/// <summary>
/// Gets a value indicating whether to include the type keyword in inferred schemas for .NET enums.
/// </summary>
Expand All @@ -32,20 +40,4 @@ public sealed class AIJsonSchemaCreateOptions
/// Gets a value indicating whether to mark all properties as required in the schema.
/// </summary>
public bool RequireAllProperties { get; init; } = true;

/// <summary>
/// Gets a value indicating whether to filter keywords that are disallowed by certain AI vendors.
/// </summary>
/// <remarks>
/// Filters a number of non-essential schema keywords that are not yet supported by some AI vendors.
/// These include:
/// <list type="bullet">
/// <item>The "minLength", "maxLength", "pattern", and "format" keywords.</item>
/// <item>The "minimum", "maximum", and "multipleOf" keywords.</item>
/// <item>The "patternProperties", "unevaluatedProperties", "propertyNames", "minProperties", and "maxProperties" keywords.</item>
/// <item>The "unevaluatedItems", "contains", "minContains", "maxContains", "minItems", "maxItems", and "uniqueItems" keywords.</item>
/// </list>
/// See also https://platform.openai.com/docs/guides/structured-outputs#some-type-specific-keywords-are-not-yet-supported.
/// </remarks>
public bool FilterDisallowedKeywords { get; init; } = true;
}
Loading

0 comments on commit 7f2d900

Please sign in to comment.