Skip to content

Commit

Permalink
Merge branch 'main' into cachecurrentprocessinprocessinfo
Browse files Browse the repository at this point in the history
  • Loading branch information
haipz authored Nov 2, 2024
2 parents 995dc27 + a12664e commit 9453453
Show file tree
Hide file tree
Showing 45 changed files with 4,863 additions and 64 deletions.
4 changes: 4 additions & 0 deletions eng/MSBuild/LegacySupport.props
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\StringSyntaxAttribute\*.cs" LinkBase="LegacySupport\StringSyntaxAttribute" />
</ItemGroup>

<ItemGroup Condition="'$(InjectJsonSchemaExporterOnLegacy)' == 'true' AND ('$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netcoreapp3.1' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' or '$(TargetFramework)' == 'net8.0')">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\JsonSchemaExporter\**\*.cs" LinkBase="Shared\EmptyCollections" />
</ItemGroup>

<ItemGroup Condition="'$(InjectGetOrAddOnLegacy)' == 'true' AND ('$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0')">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\GetOrAdd\*.cs" LinkBase="LegacySupport\GetOrAdd" />
</ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions eng/packages/TestOnly.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<PackageVersion Include="Verify.Xunit" Version="20.4.0" />
<PackageVersion Include="Xunit.Combinatorial" Version="1.6.24" />
<PackageVersion Include="xunit.extensibility.execution" Version="2.4.2" />
<PackageVersion Include="JsonSchema.Net" Version="7.2.3" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Microsoft.Shared.Diagnostics;

#pragma warning disable S1144 // Unused private types or members should be removed
#pragma warning disable S2365 // Properties should not make collection or array copies
#pragma warning disable S3604 // Member initializer values should not be redundant

namespace Microsoft.Extensions.AI;

/// <summary>Provides a dictionary used as the AdditionalProperties dictionary on Microsoft.Extensions.AI objects.</summary>
[DebuggerTypeProxy(typeof(DebugView))]
[DebuggerDisplay("Count = {Count}")]
public sealed class AdditionalPropertiesDictionary : IDictionary<string, object?>, IReadOnlyDictionary<string, object?>
{
/// <summary>The underlying dictionary.</summary>
Expand Down Expand Up @@ -77,6 +85,25 @@ public object? this[string key]
/// <inheritdoc />
public void Add(string key, object? value) => _dictionary.Add(key, value);

/// <summary>Attempts to add the specified key and value to the dictionary.</summary>
/// <param name="key">The key of the element to add.</param>
/// <param name="value">The value of the element to add.</param>
/// <returns><see langword="true"/> if the key/value pair was added to the dictionary successfully; otherwise, <see langword="false"/>.</returns>
public bool TryAdd(string key, object? value)
{
#if NET
return _dictionary.TryAdd(key, value);
#else
if (!_dictionary.ContainsKey(key))
{
_dictionary.Add(key, value);
return true;
}

return false;
#endif
}

/// <inheritdoc />
void ICollection<KeyValuePair<string, object?>>.Add(KeyValuePair<string, object?> item) => ((ICollection<KeyValuePair<string, object?>>)_dictionary).Add(item);

Expand All @@ -93,11 +120,17 @@ public object? this[string key]
void ICollection<KeyValuePair<string, object?>>.CopyTo(KeyValuePair<string, object?>[] array, int arrayIndex) =>
((ICollection<KeyValuePair<string, object?>>)_dictionary).CopyTo(array, arrayIndex);

/// <summary>
/// Returns an enumerator that iterates through the <see cref="AdditionalPropertiesDictionary"/>.
/// </summary>
/// <returns>An <see cref="AdditionalPropertiesDictionary.Enumerator"/> that enumerates the contents of the <see cref="AdditionalPropertiesDictionary"/>.</returns>
public Enumerator GetEnumerator() => new(_dictionary.GetEnumerator());

/// <inheritdoc />
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator() => _dictionary.GetEnumerator();
IEnumerator<KeyValuePair<string, object?>> IEnumerable<KeyValuePair<string, object?>>.GetEnumerator() => GetEnumerator();

/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => _dictionary.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

/// <inheritdoc />
public bool Remove(string key) => _dictionary.Remove(key);
Expand Down Expand Up @@ -156,4 +189,59 @@ public bool TryGetValue<T>(string key, [NotNullWhen(true)] out T? value)
value = default;
return false;
}

/// <summary>Enumerates the elements of an <see cref="AdditionalPropertiesDictionary"/>.</summary>
public struct Enumerator : IEnumerator<KeyValuePair<string, object?>>
{
/// <summary>The wrapped dictionary enumerator.</summary>
private Dictionary<string, object?>.Enumerator _dictionaryEnumerator;

/// <summary>Initializes a new instance of the <see cref="Enumerator"/> struct with the dictionary enumerator to wrap.</summary>
/// <param name="dictionaryEnumerator">The dictionary enumerator to wrap.</param>
internal Enumerator(Dictionary<string, object?>.Enumerator dictionaryEnumerator)
{
_dictionaryEnumerator = dictionaryEnumerator;
}

/// <inheritdoc />
public KeyValuePair<string, object?> Current => _dictionaryEnumerator.Current;

/// <inheritdoc />
object IEnumerator.Current => Current;

/// <inheritdoc />
public void Dispose() => _dictionaryEnumerator.Dispose();

/// <inheritdoc />
public bool MoveNext() => _dictionaryEnumerator.MoveNext();

/// <inheritdoc />
public void Reset() => Reset(ref _dictionaryEnumerator);

/// <summary>Calls <see cref="IEnumerator.Reset"/> on an enumerator.</summary>
private static void Reset<TEnumerator>(ref TEnumerator enumerator)
where TEnumerator : struct, IEnumerator
{
enumerator.Reset();
}
}

/// <summary>Provides a debugger view for the collection.</summary>
private sealed class DebugView(AdditionalPropertiesDictionary properties)
{
private readonly AdditionalPropertiesDictionary _properties = Throw.IfNull(properties);

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public AdditionalProperty[] Items => (from p in _properties select new AdditionalProperty(p.Key, p.Value)).ToArray();

[DebuggerDisplay("{Value}", Name = "[{Key}]")]
public readonly struct AdditionalProperty(string key, object? value)
{
[DebuggerBrowsable(DebuggerBrowsableState.Collapsed)]
public string Key { get; } = key;

[DebuggerBrowsable(DebuggerBrowsableState.Collapsed)]
public object? Value { get; } = value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public class ChatOptions
/// <summary>Gets or sets the presence penalty for generating chat responses.</summary>
public float? PresencePenalty { get; set; }

/// <summary>Gets or sets a seed value used by a service to control the reproducability of results.</summary>
public long? Seed { get; set; }

/// <summary>
/// Gets or sets the response format for the chat request.
/// </summary>
Expand Down Expand Up @@ -74,6 +77,7 @@ public virtual ChatOptions Clone()
TopK = TopK,
FrequencyPenalty = FrequencyPenalty,
PresencePenalty = PresencePenalty,
Seed = Seed,
ResponseFormat = ResponseFormat,
ModelId = ModelId,
ToolMode = ToolMode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
</PropertyGroup>

<PropertyGroup>
<InjectJsonSchemaExporterOnLegacy>true</InjectJsonSchemaExporterOnLegacy>
<InjectSharedEmptyCollections>true</InjectSharedEmptyCollections>
<InjectStringHashOnLegacy>true</InjectStringHashOnLegacy>
<InjectStringSyntaxAttributeOnLegacy>true</InjectStringSyntaxAttributeOnLegacy>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics;
#if !NET9_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Schema;
using System.Text.Json.Serialization;
using Microsoft.Shared.Diagnostics;

#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
#pragma warning disable S107 // Methods should not have too many parameters
#pragma warning disable S1075 // URIs should not be hardcoded
#pragma warning disable SA1118 // Parameter should not span multiple lines

using FunctionParameterKey = (
System.Type? Type,
Expand Down Expand Up @@ -138,8 +143,6 @@ public static JsonElement CreateJsonSchema(
JsonSerializerOptions? serializerOptions = null,
AIJsonSchemaCreateOptions? inferenceOptions = null)
{
_ = Throw.IfNull(serializerOptions);

serializerOptions ??= DefaultOptions;
inferenceOptions ??= AIJsonSchemaCreateOptions.Default;

Expand Down Expand Up @@ -176,6 +179,11 @@ private static JsonElement GetJsonSchemaCached(JsonSerializerOptions options, Fu
#endif
}

#if !NET9_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access",
Justification = "Pre STJ-9 schema extraction can fail with a runtime exception if certain reflection metadata have been trimmed. " +
"The exception message will guide users to turn off 'IlcTrimMetadata' which resolves all issues.")]
#endif
private static JsonElement GetJsonSchemaCore(JsonSerializerOptions options, FunctionParameterKey key)
{
_ = Throw.IfNull(options);
Expand Down Expand Up @@ -238,16 +246,9 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext ctx, JsonNode schema)
const string DefaultPropertyName = "default";
const string RefPropertyName = "$ref";

// Find the first DescriptionAttribute, starting first from the property, then the parameter, and finally the type itself.
Type descAttrType = typeof(DescriptionAttribute);
var descriptionAttribute =
GetAttrs(descAttrType, ctx.PropertyInfo?.AttributeProvider)?.FirstOrDefault() ??
GetAttrs(descAttrType, ctx.PropertyInfo?.AssociatedParameter?.AttributeProvider)?.FirstOrDefault() ??
GetAttrs(descAttrType, ctx.TypeInfo.Type)?.FirstOrDefault();

if (descriptionAttribute is DescriptionAttribute attr)
if (ctx.ResolveAttribute<DescriptionAttribute>() is { } attr)
{
ConvertSchemaToObject(ref schema).Insert(0, DescriptionPropertyName, (JsonNode)attr.Description);
ConvertSchemaToObject(ref schema).InsertAtStart(DescriptionPropertyName, (JsonNode)attr.Description);
}

if (schema is JsonObject objSchema)
Expand All @@ -270,32 +271,32 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext ctx, JsonNode schema)
// Include the type keyword in enum types
if (key.IncludeTypeInEnumSchemas && ctx.TypeInfo.Type.IsEnum && objSchema.ContainsKey(EnumPropertyName) && !objSchema.ContainsKey(TypePropertyName))
{
objSchema.Insert(0, TypePropertyName, "string");
objSchema.InsertAtStart(TypePropertyName, "string");
}

// Disallow additional properties in object schemas
if (key.DisallowAdditionalProperties && objSchema.ContainsKey(PropertiesPropertyName) && !objSchema.ContainsKey(AdditionalPropertiesPropertyName))
{
objSchema.Add(AdditionalPropertiesPropertyName, (JsonNode)false);
}
}

if (ctx.Path.IsEmpty)
{
// We are at the root-level schema node, update/append parameter-specific metadata

// Some consumers of the JSON schema, including Ollama as of v0.3.13, don't understand
// schemas with "type": [...], and only understand "type" being a single value.
// STJ represents .NET integer types as ["string", "integer"], which will then lead to an error.
if (TypeIsArrayContainingInteger(schema))
if (TypeIsIntegerWithStringNumberHandling(ctx, objSchema))
{
// We don't want to emit any array for "type". In this case we know it contains "integer"
// so reduce the type to that alone, assuming it's the most specific type.
// This makes schemas for Int32 (etc) work with Ollama
// This makes schemas for Int32 (etc) work with Ollama.
JsonObject obj = ConvertSchemaToObject(ref schema);
obj[TypePropertyName] = "integer";
_ = obj.Remove(PatternPropertyName);
}
}

if (ctx.Path.IsEmpty)
{
// We are at the root-level schema node, update/append parameter-specific metadata

if (!string.IsNullOrWhiteSpace(key.Description))
{
Expand All @@ -305,7 +306,7 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext ctx, JsonNode schema)
if (index < 0)
{
// If there's no description property, insert it at the beginning of the doc.
obj.Insert(0, DescriptionPropertyName, (JsonNode)key.Description!);
obj.InsertAtStart(DescriptionPropertyName, (JsonNode)key.Description!);
}
else
{
Expand All @@ -323,15 +324,12 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext ctx, JsonNode schema)
if (key.IncludeSchemaUri)
{
// The $schema property must be the first keyword in the object
ConvertSchemaToObject(ref schema).Insert(0, SchemaPropertyName, (JsonNode)SchemaKeywordUri);
ConvertSchemaToObject(ref schema).InsertAtStart(SchemaPropertyName, (JsonNode)SchemaKeywordUri);
}
}

return schema;

static object[]? GetAttrs(Type attrType, ICustomAttributeProvider? provider) =>
provider?.GetCustomAttributes(attrType, inherit: false);

static JsonObject ConvertSchemaToObject(ref JsonNode schema)
{
JsonObject obj;
Expand All @@ -354,22 +352,82 @@ static JsonObject ConvertSchemaToObject(ref JsonNode schema)
}
}

private static bool TypeIsArrayContainingInteger(JsonNode schema)
private static bool TypeIsIntegerWithStringNumberHandling(JsonSchemaExporterContext ctx, JsonObject schema)
{
if (schema["type"] is JsonArray typeArray)
if (ctx.TypeInfo.NumberHandling is not JsonNumberHandling.Strict && schema["type"] is JsonArray typeArray)
{
foreach (var entry in typeArray)
int count = 0;
foreach (JsonNode? entry in typeArray)
{
if (entry?.GetValueKind() == JsonValueKind.String && entry.GetValue<string>() == "integer")
if (entry?.GetValueKind() is JsonValueKind.String &&
entry.GetValue<string>() is "integer" or "string")
{
return true;
count++;
}
}

return count == typeArray.Count;
}

return false;
}

private static void InsertAtStart(this JsonObject jsonObject, string key, JsonNode value)
{
#if NET9_0_OR_GREATER
jsonObject.Insert(0, key, value);
#else
jsonObject.Remove(key);
var copiedEntries = jsonObject.ToArray();
jsonObject.Clear();

jsonObject.Add(key, value);
foreach (var entry in copiedEntries)
{
jsonObject[entry.Key] = entry.Value;
}
#endif
}

#if !NET9_0_OR_GREATER
private static int IndexOf(this JsonObject jsonObject, string key)
{
int i = 0;
foreach (var entry in jsonObject)
{
if (string.Equals(entry.Key, key, StringComparison.Ordinal))
{
return i;
}

i++;
}

return -1;
}
#endif

private static TAttribute? ResolveAttribute<TAttribute>(this JsonSchemaExporterContext ctx)
where TAttribute : Attribute
{
// Resolve attributes from locations in the following order:
// 1. Property-level attributes
// 2. Parameter-level attributes and
// 3. Type-level attributes.
return
#if NET9_0_OR_GREATER
GetAttrs(ctx.PropertyInfo?.AttributeProvider) ??
GetAttrs(ctx.PropertyInfo?.AssociatedParameter?.AttributeProvider) ??
#else
GetAttrs(ctx.PropertyAttributeProvider) ??
GetAttrs(ctx.ParameterInfo) ??
#endif
GetAttrs(ctx.TypeInfo.Type);

static TAttribute? GetAttrs(ICustomAttributeProvider? provider) =>
(TAttribute?)provider?.GetCustomAttributes(typeof(TAttribute), inherit: false).FirstOrDefault();
}

private static JsonElement ParseJsonElement(ReadOnlySpan<byte> utf8Json)
{
Utf8JsonReader reader = new(utf8Json);
Expand Down
Loading

0 comments on commit 9453453

Please sign in to comment.